1use crate::error::RegentError;
66use crate::hosts::managed_host::InternalApiCallOutcome;
67use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
68use crate::hosts::properties::{HostProperties, OsKind};
69use crate::secrets::SecretProvidersPool;
70use crate::state::Check;
71use crate::state::attribute::HostHandler;
72use crate::state::attribute::Privilege;
73use crate::state::attribute::Remediation;
74use crate::state::attribute::RemediationsList;
75use crate::state::compliance::AttributeComplianceAssessment;
76
77use std::time::Duration;
78
79use serde::{Deserialize, Deserializer, Serialize, Serializer};
80use std::net::IpAddr;
81use std::str::FromStr;
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct CidrBlock {
86 addr: IpAddr,
87 prefix: Option<u8>,
88}
89
90impl CidrBlock {
91 pub fn parse(s: &str) -> Result<Self, String> {
93 let parts: Vec<&str> = s.split('/').collect();
94
95 if parts.is_empty() || parts.len() > 2 {
96 return Err(format!("Invalid CIDR format: '{}'", s));
97 }
98
99 let addr = IpAddr::from_str(parts[0])
100 .map_err(|e| format!("Invalid IP address '{}': {}", parts[0], e))?;
101
102 let prefix = if parts.len() == 2 {
103 let p: u8 = parts[1]
104 .parse()
105 .map_err(|_| format!("Invalid subnet prefix: '{}'", parts[1]))?;
106
107 let max_prefix = match addr {
108 IpAddr::V4(_) => 32,
109 IpAddr::V6(_) => 128,
110 };
111
112 if p > max_prefix {
113 return Err(format!(
114 "Subnet prefix /{} is out of bounds for this IP type (max {})",
115 p, max_prefix
116 ));
117 }
118 Some(p)
119 } else {
120 None
121 };
122
123 Ok(CidrBlock { addr, prefix })
124 }
125
126 pub fn to_string(&self) -> String {
127 match self.prefix {
128 Some(p) => format!("{}/{}", self.addr, p),
129 None => self.addr.to_string(),
130 }
131 }
132}
133
134impl Serialize for CidrBlock {
135 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
136 where
137 S: Serializer,
138 {
139 serializer.serialize_str(&self.to_string())
140 }
141}
142
143impl<'de> Deserialize<'de> for CidrBlock {
144 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145 where
146 D: Deserializer<'de>,
147 {
148 let s = String::deserialize(deserializer)?;
149 CidrBlock::parse(&s).map_err(serde::de::Error::custom)
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[serde(rename_all = "PascalCase")]
159pub enum IpVersion {
160 V4,
161 V6,
162}
163
164impl std::fmt::Display for IpVersion {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 match self {
167 IpVersion::V4 => write!(f, "iptables"),
168 IpVersion::V6 => write!(f, "ip6tables"),
169 }
170 }
171}
172
173fn default_ip_family() -> IpVersion {
174 IpVersion::V4
175}
176
177fn is_v4_default(family: &IpVersion) -> bool {
178 matches!(family, IpVersion::V4)
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185#[serde(rename_all = "PascalCase")]
186pub enum IptablesInsertionAction {
187 Append,
188 #[serde(rename_all = "PascalCase")]
189 Insert {
190 position: u32,
191 },
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199#[serde(rename_all = "PascalCase")]
200pub enum Protocol {
201 #[serde(rename_all = "PascalCase")]
202 Tcp {
203 #[serde(skip_serializing_if = "Option::is_none")]
204 source_port: Option<PortSpec>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 dest_port: Option<PortSpec>,
207 #[serde(skip_serializing_if = "Option::is_none")]
208 tcp_flags: Option<TcpFlagsMatch>,
209 },
210 #[serde(rename_all = "PascalCase")]
211 Udp {
212 #[serde(skip_serializing_if = "Option::is_none")]
213 source_port: Option<PortSpec>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 dest_port: Option<PortSpec>,
216 },
217 #[serde(rename_all = "PascalCase")]
218 Icmp {
219 #[serde(skip_serializing_if = "Option::is_none")]
220 icmp_type: Option<u8>,
221 #[serde(skip_serializing_if = "Option::is_none")]
222 icmp_code: Option<u8>,
223 },
224 All,
225}
226
227impl std::fmt::Display for Protocol {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 match self {
230 Protocol::Tcp { .. } => write!(f, "tcp"),
231 Protocol::Udp { .. } => write!(f, "udp"),
232 Protocol::Icmp { .. } => write!(f, "icmp"),
233 Protocol::All => write!(f, "all"),
234 }
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244#[serde(rename_all = "PascalCase")]
245pub enum TcpFlag {
246 Syn,
247 Ack,
248 Fin,
249 Rst,
250 Psh,
251 Urg,
252 FinRst,
253 All,
254 None,
255}
256
257impl std::fmt::Display for TcpFlag {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 match self {
260 TcpFlag::Syn => write!(f, "SYN"),
261 TcpFlag::Ack => write!(f, "ACK"),
262 TcpFlag::Fin => write!(f, "FIN"),
263 TcpFlag::Rst => write!(f, "RST"),
264 TcpFlag::Psh => write!(f, "PSH"),
265 TcpFlag::Urg => write!(f, "URG"),
266 TcpFlag::FinRst => write!(f, "FIN,RST"),
267 TcpFlag::All => write!(f, "ALL"),
268 TcpFlag::None => write!(f, "NONE"),
269 }
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277#[serde(rename_all = "PascalCase")]
278pub struct TcpFlagsMatch {
279 pub mask: Vec<TcpFlag>,
280 pub comp: Vec<TcpFlag>,
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
287#[serde(rename_all = "PascalCase")]
288pub struct RateLimit {
289 pub rate: u32,
290 pub unit: RateUnit,
291 pub burst: Option<u32>,
292}
293
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
300#[serde(rename_all = "PascalCase")]
301pub enum RateUnit {
302 Second,
303 Minute,
304 Hour,
305 Day,
306}
307
308impl std::fmt::Display for RateUnit {
309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 match self {
311 RateUnit::Second => write!(f, "/second"),
312 RateUnit::Minute => write!(f, "/minute"),
313 RateUnit::Hour => write!(f, "/hour"),
314 RateUnit::Day => write!(f, "/day"),
315 }
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
325#[serde(rename_all = "PascalCase")]
326pub enum ConnectionState {
327 New,
328 Established,
329 Related,
330 Invalid,
331 Untracked,
332}
333
334impl std::fmt::Display for ConnectionState {
335 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336 match self {
337 ConnectionState::New => write!(f, "NEW"),
338 ConnectionState::Established => write!(f, "ESTABLISHED"),
339 ConnectionState::Related => write!(f, "RELATED"),
340 ConnectionState::Invalid => write!(f, "INVALID"),
341 ConnectionState::Untracked => write!(f, "UNTRACKED"),
342 }
343 }
344}
345
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
348#[serde(rename_all = "PascalCase")]
349pub struct ConntrackMatch {
350 pub states: Vec<ConnectionState>,
351}
352
353#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
359#[serde(rename_all = "PascalCase")]
360pub enum OwnerMatch {
361 UidOwner(String),
362 GidOwner(String),
363}
364
365impl std::fmt::Display for OwnerMatch {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 match self {
368 OwnerMatch::UidOwner(uid) => write!(f, "--uid-owner {}", uid),
369 OwnerMatch::GidOwner(gid) => write!(f, "--gid-owner {}", gid),
370 }
371 }
372}
373
374#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
379#[serde(rename_all = "PascalCase")]
380pub struct MatchCriteria {
381 pub protocol: Protocol,
382
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub source: Option<Invert<CidrBlock>>,
385
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub destination: Option<Invert<CidrBlock>>,
388
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub network_interface_in: Option<Invert<String>>,
391
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub network_interface_out: Option<Invert<String>>,
394
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub fragment: Option<bool>,
397
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub limit: Option<RateLimit>,
400
401 #[serde(skip_serializing_if = "Option::is_none")]
402 pub conntrack: Option<Invert<ConntrackMatch>>,
403
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub owner: Option<Invert<OwnerMatch>>,
406
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub comment: Option<String>,
409}
410
411#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
417#[serde(rename_all = "PascalCase")]
418pub enum RejectWith {
419 IcmpPortUnreachable,
420 IcmpNetUnreachable,
421 TcpReset,
422 EchoReply,
423}
424
425impl std::fmt::Display for RejectWith {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 match self {
428 RejectWith::IcmpPortUnreachable => write!(f, "icmp-port-unreachable"),
429 RejectWith::IcmpNetUnreachable => write!(f, "icmp-net-unreachable"),
430 RejectWith::TcpReset => write!(f, "tcp-reset"),
431 RejectWith::EchoReply => write!(f, "echo-reply"),
432 }
433 }
434}
435
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
441#[serde(rename_all = "PascalCase")]
442pub enum IptablesTarget {
443 Accept,
444 Drop,
445 #[serde(rename_all = "PascalCase")]
446 Reject {
447 #[serde(skip_serializing_if = "Option::is_none")]
448 with: Option<RejectWith>,
449 },
450 #[serde(rename_all = "PascalCase")]
451 Log {
452 #[serde(skip_serializing_if = "Option::is_none")]
453 prefix: Option<String>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 level: Option<u8>,
456 },
457 Return,
458 Jump(String),
459 Goto(String),
460 Custom(String),
461}
462
463#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
469#[serde(rename_all = "PascalCase")]
470pub enum RawChain {
471 Prerouting,
472 Output,
473 Custom(String),
474}
475
476impl std::fmt::Display for RawChain {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 match self {
479 RawChain::Prerouting => write!(f, "PREROUTING"),
480 RawChain::Output => write!(f, "OUTPUT"),
481 RawChain::Custom(s) => write!(f, "{}", s),
482 }
483 }
484}
485
486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
492#[serde(rename_all = "PascalCase")]
493pub enum FilterChain {
494 Input,
495 Forward,
496 Output,
497 Custom(String),
498}
499
500impl std::fmt::Display for FilterChain {
501 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502 match self {
503 FilterChain::Input => write!(f, "INPUT"),
504 FilterChain::Forward => write!(f, "FORWARD"),
505 FilterChain::Output => write!(f, "OUTPUT"),
506 FilterChain::Custom(s) => write!(f, "{}", s),
507 }
508 }
509}
510
511#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
517#[serde(rename_all = "PascalCase")]
518pub enum NatChain {
519 Prerouting,
520 Input,
521 Output,
522 Postrouting,
523 Custom(String),
524}
525
526impl std::fmt::Display for NatChain {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 match self {
529 NatChain::Prerouting => write!(f, "PREROUTING"),
530 NatChain::Input => write!(f, "INPUT"),
531 NatChain::Output => write!(f, "OUTPUT"),
532 NatChain::Postrouting => write!(f, "POSTROUTING"),
533 NatChain::Custom(s) => write!(f, "{}", s),
534 }
535 }
536}
537
538#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
545#[serde(rename_all = "PascalCase")]
546pub enum MangleChain {
547 Prerouting,
548 Input,
549 Forward,
550 Output,
551 Postrouting,
552 Custom(String),
553}
554
555impl std::fmt::Display for MangleChain {
556 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557 match self {
558 MangleChain::Prerouting => write!(f, "PREROUTING"),
559 MangleChain::Input => write!(f, "INPUT"),
560 MangleChain::Forward => write!(f, "FORWARD"),
561 MangleChain::Output => write!(f, "OUTPUT"),
562 MangleChain::Postrouting => write!(f, "POSTROUTING"),
563 MangleChain::Custom(s) => write!(f, "{}", s),
564 }
565 }
566}
567
568#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
574#[serde(rename_all = "PascalCase")]
575pub enum SecurityChain {
576 Input,
577 Forward,
578 Output,
579 Custom(String),
580}
581
582impl std::fmt::Display for SecurityChain {
583 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584 match self {
585 SecurityChain::Input => write!(f, "INPUT"),
586 SecurityChain::Forward => write!(f, "FORWARD"),
587 SecurityChain::Output => write!(f, "OUTPUT"),
588 SecurityChain::Custom(s) => write!(f, "{}", s),
589 }
590 }
591}
592
593#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
597#[serde(tag = "Table", content = "Details", rename_all = "PascalCase")]
598pub enum IptablesRule {
599 #[serde(rename_all = "PascalCase")]
600 Raw {
601 chain: RawChain,
602 criteria: MatchCriteria,
603 target: IptablesTarget,
604 },
605 #[serde(rename_all = "PascalCase")]
606 Filter {
607 chain: FilterChain,
608 criteria: MatchCriteria,
609 target: IptablesTarget,
610 },
611 #[serde(rename_all = "PascalCase")]
612 Nat {
613 chain: NatChain,
614 criteria: MatchCriteria,
615 target: IptablesTarget,
616 },
617 #[serde(rename_all = "PascalCase")]
618 Mangle {
619 chain: MangleChain,
620 criteria: MatchCriteria,
621 target: IptablesTarget,
622 },
623 #[serde(rename_all = "PascalCase")]
624 Security {
625 chain: SecurityChain,
626 criteria: MatchCriteria,
627 target: IptablesTarget,
628 },
629}
630
631#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
635#[serde(rename_all = "PascalCase")]
636pub enum RuleExpectedState {
637 Present,
638 Absent,
639}
640
641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
646#[serde(rename_all = "PascalCase")]
647pub struct Invert<T> {
648 pub value: T,
649 #[serde(
650 rename = "Inverted",
651 default,
652 skip_serializing_if = "std::ops::Not::not"
653 )]
654 pub inverted: bool,
655}
656
657impl<T> Invert<T> {
658 pub fn new(value: T) -> Self {
659 Self {
660 value,
661 inverted: false,
662 }
663 }
664
665 pub fn inverted(value: T) -> Self {
666 Self {
667 value,
668 inverted: true,
669 }
670 }
671}
672
673#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678#[serde(rename_all = "PascalCase", untagged)]
679pub enum PortRange {
680 Single(u16),
681 Range {
682 #[serde(rename = "Start")]
683 start: u16,
684 #[serde(rename = "End")]
685 end: u16,
686 },
687}
688
689impl std::fmt::Display for PortRange {
690 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691 match self {
692 PortRange::Single(port) => write!(f, "{}", port),
693 PortRange::Range { start, end } => write!(f, "{}:{}", start, end),
694 }
695 }
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
704#[serde(rename_all = "PascalCase", untagged)]
705pub enum PortSpec {
706 Single(u16),
707 Range {
708 #[serde(rename = "Start")]
709 start: u16,
710 #[serde(rename = "End")]
711 end: u16,
712 },
713 List(Vec<PortRange>),
714}
715
716impl std::fmt::Display for PortSpec {
717 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718 match self {
719 PortSpec::Single(port) => write!(f, "{}", port),
720 PortSpec::Range { start, end } => write!(f, "{}:{}", start, end),
721 PortSpec::List(ports) => {
722 let port_strs: Vec<String> = ports.iter().map(|p| p.to_string()).collect();
723 write!(f, "{}", port_strs.join(","))
724 }
725 }
726 }
727}
728
729#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
734#[serde(
735 tag = "State",
736 rename_all = "PascalCase",
737 rename_all_fields = "PascalCase"
738)]
739pub enum IptablesExpectedState {
740 Present {
741 #[serde(default = "default_ip_family", skip_serializing_if = "is_v4_default")]
742 ip_version: IpVersion,
743 action: IptablesInsertionAction,
744 #[serde(flatten)]
745 rule: IptablesRule,
746 },
747 Absent {
748 #[serde(default = "default_ip_family")]
749 ip_version: IpVersion,
750 #[serde(flatten)]
751 rule: IptablesRule,
752 },
753}
754
755impl IptablesExpectedState {
816 pub fn present(
817 ip_version: IpVersion,
818 action: IptablesInsertionAction,
819 rule: IptablesRule,
820 ) -> IptablesExpectedState {
821 IptablesExpectedState::Present {
822 ip_version,
823 action,
824 rule,
825 }
826 }
827
828 pub fn absent(ip_version: IpVersion, rule: IptablesRule) -> IptablesExpectedState {
829 IptablesExpectedState::Absent { ip_version, rule }
830 }
831
832 pub fn appended(rule: IptablesRule) -> IptablesExpectedState {
834 Self::present(IpVersion::V4, IptablesInsertionAction::Append, rule)
835 }
836
837 pub fn inserted_at(position: u32, rule: IptablesRule) -> IptablesExpectedState {
839 Self::present(
840 IpVersion::V4,
841 IptablesInsertionAction::Insert { position },
842 rule,
843 )
844 }
845
846 pub fn removed(rule: IptablesRule) -> IptablesExpectedState {
848 Self::absent(IpVersion::V4, rule)
849 }
850
851 pub fn for_ipv6(self) -> IptablesExpectedState {
853 match self {
854 IptablesExpectedState::Present { action, rule, .. } => IptablesExpectedState::Present {
855 ip_version: IpVersion::V6,
856 action,
857 rule,
858 },
859 IptablesExpectedState::Absent { rule, .. } => IptablesExpectedState::Absent {
860 ip_version: IpVersion::V6,
861 rule,
862 },
863 }
864 }
865
866 pub fn allow_tcp_port(port: u16, position: Option<u32>) -> IptablesExpectedState {
869 Self::allow_tcp_port_with_ip(port, IpVersion::V4, position)
870 }
871
872 pub fn allow_tcp_port_with_ip(
874 port: u16,
875 ip_version: IpVersion,
876 position: Option<u32>,
877 ) -> IptablesExpectedState {
878 let action = match position {
879 Some(pos) => IptablesInsertionAction::Insert { position: pos },
880 None => IptablesInsertionAction::Append,
881 };
882 IptablesExpectedState::Present {
883 ip_version,
884 action,
885 rule: IptablesRule::Filter {
886 chain: FilterChain::Input,
887 criteria: MatchCriteria {
888 protocol: Protocol::Tcp {
889 source_port: None,
890 dest_port: Some(PortSpec::Single(port)),
891 tcp_flags: None,
892 },
893 source: None,
894 destination: None,
895 network_interface_in: None,
896 network_interface_out: None,
897 fragment: None,
898 limit: None,
899 conntrack: None,
900 owner: None,
901 comment: None,
902 },
903 target: IptablesTarget::Accept,
904 },
905 }
906 }
907
908 pub fn allow_udp_port(port: u16, position: Option<u32>) -> IptablesExpectedState {
911 Self::allow_udp_port_with_ip(port, IpVersion::V4, position)
912 }
913
914 pub fn allow_udp_port_with_ip(
916 port: u16,
917 ip_version: IpVersion,
918 position: Option<u32>,
919 ) -> IptablesExpectedState {
920 let action = match position {
921 Some(pos) => IptablesInsertionAction::Insert { position: pos },
922 None => IptablesInsertionAction::Append,
923 };
924 IptablesExpectedState::Present {
925 ip_version,
926 action,
927 rule: IptablesRule::Filter {
928 chain: FilterChain::Input,
929 criteria: MatchCriteria {
930 protocol: Protocol::Udp {
931 source_port: None,
932 dest_port: Some(PortSpec::Single(port)),
933 },
934 source: None,
935 destination: None,
936 network_interface_in: None,
937 network_interface_out: None,
938 fragment: None,
939 limit: None,
940 conntrack: None,
941 owner: None,
942 comment: None,
943 },
944 target: IptablesTarget::Accept,
945 },
946 }
947 }
948
949 pub fn allow_ssh(position: Option<u32>) -> IptablesExpectedState {
952 Self::allow_ssh_with_ip(IpVersion::V4, position)
953 }
954
955 pub fn allow_ssh_with_ip(
957 ip_version: IpVersion,
958 position: Option<u32>,
959 ) -> IptablesExpectedState {
960 Self::allow_tcp_port_with_ip(22, ip_version, position)
961 }
962
963 pub fn drop_all_incoming(position: Option<u32>) -> IptablesExpectedState {
966 Self::drop_all_incoming_with_ip(IpVersion::V4, position)
967 }
968
969 pub fn drop_all_incoming_with_ip(
971 ip_version: IpVersion,
972 position: Option<u32>,
973 ) -> IptablesExpectedState {
974 let action = match position {
975 Some(pos) => IptablesInsertionAction::Insert { position: pos },
976 None => IptablesInsertionAction::Append,
977 };
978 IptablesExpectedState::Present {
979 ip_version,
980 action,
981 rule: IptablesRule::Filter {
982 chain: FilterChain::Input,
983 criteria: MatchCriteria {
984 protocol: Protocol::All,
985 source: None,
986 destination: None,
987 network_interface_in: None,
988 network_interface_out: None,
989 fragment: None,
990 limit: None,
991 conntrack: None,
992 owner: None,
993 comment: None,
994 },
995 target: IptablesTarget::Drop,
996 },
997 }
998 }
999}
1000
1001impl Timeout for IptablesExpectedState {
1002 fn default_timeout(&self) -> Duration {
1003 Duration::from_secs(5)
1004 }
1005}
1006
1007impl Check for IptablesExpectedState {
1010 fn check(&self) -> Result<(), RegentError> {
1011 let rule = match self {
1012 IptablesExpectedState::Present { rule, .. }
1013 | IptablesExpectedState::Absent { rule, .. } => rule,
1014 };
1015
1016 match rule {
1017 IptablesRule::Raw {
1018 chain: RawChain::Custom(c),
1019 ..
1020 }
1021 | IptablesRule::Filter {
1022 chain: FilterChain::Custom(c),
1023 ..
1024 }
1025 | IptablesRule::Nat {
1026 chain: NatChain::Custom(c),
1027 ..
1028 }
1029 | IptablesRule::Mangle {
1030 chain: MangleChain::Custom(c),
1031 ..
1032 }
1033 | IptablesRule::Security {
1034 chain: SecurityChain::Custom(c),
1035 ..
1036 } if c.is_empty() => {
1037 return Err(RegentError::IncoherentExpectedState(
1038 "custom chain name must not be empty.".to_string(),
1039 ));
1040 }
1041 _ => {}
1042 }
1043
1044 if let IptablesExpectedState::Present { action, .. } = self
1045 && let IptablesInsertionAction::Insert { position: _ } = action
1046 {
1047 }
1049
1050 Ok(())
1051 }
1052
1053 fn check_host_compatibility(
1054 &self,
1055 host_properties: &HostProperties,
1056 ) -> Result<(), RegentError> {
1057 match host_properties.os_kind() {
1058 OsKind::Linux(_) => Ok(()),
1059 incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
1060 "Host is {:?} but iptables is only supported on Linux",
1061 incompatible_os_kind
1062 ))),
1063 }
1064 }
1065}
1066
1067fn get_table_arg(rule: &IptablesRule) -> String {
1071 match rule {
1072 IptablesRule::Raw {
1073 chain: _,
1074 criteria: _,
1075 target: _,
1076 } => "-t raw",
1077 IptablesRule::Filter {
1078 chain: _,
1079 criteria: _,
1080 target: _,
1081 } => "-t filter",
1082 IptablesRule::Nat {
1083 chain: _,
1084 criteria: _,
1085 target: _,
1086 } => "-t nat",
1087 IptablesRule::Mangle {
1088 chain: _,
1089 criteria: _,
1090 target: _,
1091 } => "-t mangle",
1092 IptablesRule::Security {
1093 chain: _,
1094 criteria: _,
1095 target: _,
1096 } => "-t security",
1097 }
1098 .to_string()
1099}
1100
1101fn get_chain_name(rule: &IptablesRule) -> String {
1103 match rule {
1104 IptablesRule::Raw { chain, .. } => chain.to_string(),
1105 IptablesRule::Filter { chain, .. } => chain.to_string(),
1106 IptablesRule::Nat { chain, .. } => chain.to_string(),
1107 IptablesRule::Mangle { chain, .. } => chain.to_string(),
1108 IptablesRule::Security { chain, .. } => chain.to_string(),
1109 }
1110}
1111
1112fn ip_version_to_str(ip_version: &IpVersion) -> String {
1114 ip_version.to_string()
1115}
1116
1117impl<Handler: HostHandler> AssessCompliance<Handler> for IptablesExpectedState {
1118 async fn assess_compliance(
1119 &self,
1120 host_handler: &mut Handler,
1121 host_properties: &Option<HostProperties>,
1122 privilege: &Privilege,
1123 _optional_secret_provider: &Option<SecretProvidersPool>,
1124 ) -> Result<AttributeComplianceAssessment, RegentError> {
1125 if let Some(props) = host_properties {
1126 self.check_host_compatibility(props)?;
1127 }
1128
1129 let (ip_version, expected_state, action, rule) = match self {
1130 IptablesExpectedState::Present {
1131 ip_version,
1132 action,
1133 rule,
1134 } => (
1135 ip_version,
1136 RuleExpectedState::Present,
1137 Some(action.clone()),
1138 rule,
1139 ),
1140 IptablesExpectedState::Absent { ip_version, rule } => {
1141 (ip_version, RuleExpectedState::Absent, None, rule)
1142 }
1143 };
1144
1145 let binary = ip_version_to_str(ip_version);
1146 let table_arg = get_table_arg(rule);
1147 let chain_name = get_chain_name(rule);
1148
1149 let mut remediations: Vec<Remediation> = Vec::new();
1150
1151 let rule_args = build_rule_args_from_new_structures(rule);
1152
1153 let check_rule_cmd = build_cmd(
1154 &binary,
1155 &table_arg,
1156 &format!("-C {} {}", chain_name, rule_args),
1157 );
1158 let check_rule_cmd = format!("{} 2>/dev/null", check_rule_cmd);
1159 let check_result = host_handler
1163 .run_command(&check_rule_cmd, privilege)
1164 .await
1165 .unwrap();
1166
1167 match expected_state {
1168 RuleExpectedState::Present => {
1169 if check_result.return_code != 0 {
1170 let insertion_action = action.unwrap_or(IptablesInsertionAction::Append);
1171 match insertion_action {
1172 IptablesInsertionAction::Append => {
1173 remediations.push(Remediation::Iptables(IptablesApiCall::from(
1174 IptablesModuleInternalApiCall::AppendRule {
1175 binary: binary.to_string(),
1176 table_arg: table_arg.clone(),
1177 chain: chain_name.clone(),
1178 rule_args: rule_args.clone(),
1179 },
1180 privilege.clone(),
1181 )));
1182 }
1183 IptablesInsertionAction::Insert { position } => {
1184 remediations.push(Remediation::Iptables(IptablesApiCall::from(
1185 IptablesModuleInternalApiCall::InsertRule {
1186 binary: binary.to_string(),
1187 table_arg: table_arg.clone(),
1188 chain: chain_name.clone(),
1189 rule_num: Some(position),
1190 rule_args: rule_args.clone(),
1191 },
1192 privilege.clone(),
1193 )));
1194 }
1195 }
1196 }
1197 }
1198 RuleExpectedState::Absent => {
1199 if check_result.return_code == 0 {
1200 remediations.push(Remediation::Iptables(IptablesApiCall::from(
1201 IptablesModuleInternalApiCall::DeleteRule {
1202 binary: binary.to_string(),
1203 table_arg: table_arg.clone(),
1204 chain: chain_name.clone(),
1205 rule_args: rule_args.clone(),
1206 },
1207 privilege.clone(),
1208 )));
1209 }
1210 }
1211 }
1212
1213 if remediations.is_empty() {
1214 Ok(AttributeComplianceAssessment::Compliant)
1215 } else {
1216 Ok(AttributeComplianceAssessment::NonCompliant(
1217 RemediationsList::from(remediations)?,
1218 ))
1219 }
1220 }
1221}
1222
1223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1226#[serde(rename_all = "PascalCase")]
1227pub enum IptablesModuleInternalApiCall {
1228 CreateChain {
1229 binary: String,
1230 table_arg: String,
1231 chain: String,
1232 },
1233 FlushAndDeleteChain {
1234 binary: String,
1235 table_arg: String,
1236 chain: String,
1237 },
1238 SetPolicy {
1239 binary: String,
1240 table_arg: String,
1241 chain: String,
1242 policy: String,
1243 },
1244 AppendRule {
1245 binary: String,
1246 table_arg: String,
1247 chain: String,
1248 rule_args: String,
1249 },
1250 InsertRule {
1251 binary: String,
1252 table_arg: String,
1253 chain: String,
1254 rule_num: Option<u32>,
1255 rule_args: String,
1256 },
1257 DeleteRule {
1258 binary: String,
1259 table_arg: String,
1260 chain: String,
1261 rule_args: String,
1262 },
1263}
1264
1265impl std::fmt::Display for IptablesModuleInternalApiCall {
1266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1267 match self {
1268 IptablesModuleInternalApiCall::CreateChain {
1269 binary,
1270 table_arg,
1271 chain,
1272 } => {
1273 write!(f, "{} create chain {}", binary, chain)?;
1274 if !table_arg.is_empty() {
1275 write!(f, " ({})", table_arg)?;
1276 }
1277 Ok(())
1278 }
1279 IptablesModuleInternalApiCall::FlushAndDeleteChain {
1280 binary,
1281 table_arg,
1282 chain,
1283 } => {
1284 write!(f, "{} flush and delete chain {}", binary, chain)?;
1285 if !table_arg.is_empty() {
1286 write!(f, " ({})", table_arg)?;
1287 }
1288 Ok(())
1289 }
1290 IptablesModuleInternalApiCall::SetPolicy {
1291 binary,
1292 table_arg,
1293 chain,
1294 policy,
1295 } => {
1296 write!(f, "{} set policy {} on chain {}", binary, policy, chain)?;
1297 if !table_arg.is_empty() {
1298 write!(f, " ({})", table_arg)?;
1299 }
1300 Ok(())
1301 }
1302 IptablesModuleInternalApiCall::AppendRule {
1303 binary,
1304 table_arg,
1305 chain,
1306 rule_args,
1307 } => {
1308 write!(f, "{} append rule to {} : {}", binary, chain, rule_args)?;
1309 if !table_arg.is_empty() {
1310 write!(f, " ({})", table_arg)?;
1311 }
1312 Ok(())
1313 }
1314 IptablesModuleInternalApiCall::InsertRule {
1315 binary,
1316 table_arg,
1317 chain,
1318 rule_num,
1319 rule_args,
1320 } => {
1321 match rule_num {
1322 Some(n) => write!(
1323 f,
1324 "{} insert rule at {} in {} : {}",
1325 binary, n, chain, rule_args
1326 )?,
1327 None => write!(f, "{} insert rule in {} : {}", binary, chain, rule_args)?,
1328 }
1329 if !table_arg.is_empty() {
1330 write!(f, " ({})", table_arg)?;
1331 }
1332 Ok(())
1333 }
1334 IptablesModuleInternalApiCall::DeleteRule {
1335 binary,
1336 table_arg,
1337 chain,
1338 rule_args,
1339 } => {
1340 write!(f, "{} delete rule from {} : {}", binary, chain, rule_args)?;
1341 if !table_arg.is_empty() {
1342 write!(f, " ({})", table_arg)?;
1343 }
1344 Ok(())
1345 }
1346 }
1347 }
1348}
1349
1350#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1353pub struct IptablesApiCall {
1354 pub api_call: IptablesModuleInternalApiCall,
1355 privilege: Privilege,
1356}
1357
1358impl IptablesApiCall {
1359 pub fn display(&self) -> String {
1360 format!("{}", self.api_call)
1361 }
1362
1363 fn from(api_call: IptablesModuleInternalApiCall, privilege: Privilege) -> IptablesApiCall {
1364 IptablesApiCall {
1365 api_call,
1366 privilege,
1367 }
1368 }
1369}
1370
1371impl Check for IptablesApiCall {
1372 fn check(&self) -> Result<(), RegentError> {
1373 Ok(())
1374 }
1375
1376 fn check_host_compatibility(
1377 &self,
1378 host_properties: &HostProperties,
1379 ) -> Result<(), RegentError> {
1380 match host_properties.os_kind() {
1381 OsKind::Linux(_) => Ok(()),
1382 incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
1383 "Host is {:?} but iptables is only supported on Linux",
1384 incompatible_os_kind
1385 ))),
1386 }
1387 }
1388}
1389
1390impl<Handler: HostHandler> ReachCompliance<Handler> for IptablesApiCall {
1391 async fn call(
1392 &self,
1393 host_handler: &mut Handler,
1394 host_properties: &Option<HostProperties>,
1395 _optional_secret_provider: &Option<SecretProvidersPool>,
1396 ) -> Result<InternalApiCallOutcome, RegentError> {
1397 if let Some(props) = host_properties {
1398 self.check_host_compatibility(props)?;
1399 }
1400
1401 let cmd = match &self.api_call {
1402 IptablesModuleInternalApiCall::CreateChain {
1403 binary,
1404 table_arg,
1405 chain,
1406 } => build_cmd(binary, table_arg, &format!("-N {}", chain)),
1407 IptablesModuleInternalApiCall::FlushAndDeleteChain {
1408 binary,
1409 table_arg,
1410 chain,
1411 } => {
1412 let flush = build_cmd(binary, table_arg, &format!("-F {}", chain));
1413 let delete = build_cmd(binary, table_arg, &format!("-X {}", chain));
1414 format!("{} && {}", flush, delete)
1415 }
1416 IptablesModuleInternalApiCall::SetPolicy {
1417 binary,
1418 table_arg,
1419 chain,
1420 policy,
1421 } => build_cmd(binary, table_arg, &format!("-P {} {}", chain, policy)),
1422 IptablesModuleInternalApiCall::AppendRule {
1423 binary,
1424 table_arg,
1425 chain,
1426 rule_args,
1427 } => build_cmd(binary, table_arg, &format!("-A {} {}", chain, rule_args)),
1428 IptablesModuleInternalApiCall::InsertRule {
1429 binary,
1430 table_arg,
1431 chain,
1432 rule_num,
1433 rule_args,
1434 } => match rule_num {
1435 Some(n) => build_cmd(
1436 binary,
1437 table_arg,
1438 &format!("-I {} {} {}", chain, n, rule_args),
1439 ),
1440 None => build_cmd(binary, table_arg, &format!("-I {} {}", chain, rule_args)),
1441 },
1442 IptablesModuleInternalApiCall::DeleteRule {
1443 binary,
1444 table_arg,
1445 chain,
1446 rule_args,
1447 } => build_cmd(binary, table_arg, &format!("-D {} {}", chain, rule_args)),
1448 };
1449
1450 let result = host_handler
1451 .run_command(&cmd, &self.privilege)
1452 .await
1453 .unwrap();
1454
1455 if result.return_code == 0 {
1456 let verification_result =
1457 verify_rule_position(host_handler, &self.api_call, &self.privilege).await;
1458
1459 if verification_result {
1460 Ok(InternalApiCallOutcome::Success(None))
1461 } else {
1462 Ok(InternalApiCallOutcome::Failure(
1463 "Command succeeded but post-verification failed: rule position does not match expected".to_string(),
1464 ))
1465 }
1466 } else {
1467 Ok(InternalApiCallOutcome::Failure(format!(
1468 "RC: {}, STDOUT: {}, STDERR: {}",
1469 result.return_code, result.stdout, result.stderr
1470 )))
1471 }
1472 }
1473}
1474
1475async fn verify_rule_position<Handler: HostHandler>(
1478 host_handler: &mut Handler,
1479 api_call: &IptablesModuleInternalApiCall,
1480 privilege: &Privilege,
1481) -> bool {
1482 match api_call {
1483 IptablesModuleInternalApiCall::AppendRule {
1484 binary,
1485 table_arg,
1486 chain,
1487 rule_args,
1488 } => {
1489 let check_cmd = build_cmd(binary, table_arg, &format!("-C {} {}", chain, rule_args));
1490 let check_result = host_handler
1491 .run_command(&check_cmd, privilege)
1492 .await
1493 .unwrap();
1494 check_result.return_code == 0
1495 }
1496 IptablesModuleInternalApiCall::InsertRule {
1497 binary,
1498 table_arg,
1499 chain,
1500 rule_num,
1501 rule_args,
1502 } => {
1503 let check_cmd = build_cmd(binary, table_arg, &format!("-C {} {}", chain, rule_args));
1504 let check_result = host_handler
1505 .run_command(&check_cmd, privilege)
1506 .await
1507 .unwrap();
1508
1509 if check_result.return_code != 0 {
1510 return false; }
1512
1513 if let Some(expected_position) = rule_num {
1514 let list_cmd = build_cmd(
1515 binary,
1516 table_arg,
1517 &format!("-L {} --line-numbers -n", chain),
1518 );
1519 let list_result = host_handler
1520 .run_command(&list_cmd, privilege)
1521 .await
1522 .unwrap();
1523
1524 if list_result.return_code == 0 {
1525 for line in list_result.stdout.lines() {
1526 if line.contains(rule_args.as_str()) {
1527 let parts: Vec<&str> = line.split_whitespace().collect();
1528 if !parts.is_empty()
1529 && let Ok(actual_position) = parts[0].parse::<u32>()
1530 {
1531 return actual_position == *expected_position;
1532 }
1533 break; }
1535 }
1536 return false; }
1538 false
1539 } else {
1540 true
1541 }
1542 }
1543 IptablesModuleInternalApiCall::DeleteRule { .. } => {
1544 true
1546 }
1547 IptablesModuleInternalApiCall::CreateChain { .. } => true,
1548 IptablesModuleInternalApiCall::FlushAndDeleteChain { .. } => true,
1549 IptablesModuleInternalApiCall::SetPolicy { .. } => {
1550 true
1553 }
1554 }
1555}
1556
1557fn build_cmd(binary: &str, table_arg: &str, flags: &str) -> String {
1563 if table_arg.is_empty() {
1564 format!("{} {}", binary, flags)
1565 } else {
1566 format!("{} {} {}", binary, table_arg, flags)
1567 }
1568}
1569
1570fn protocol_to_str(proto: &Protocol) -> String {
1575 proto.to_string()
1576}
1577
1578fn port_spec_to_str(port: &PortSpec) -> String {
1580 port.to_string()
1581}
1582
1583fn tcp_flag_to_str(flag: &TcpFlag) -> &'static str {
1585 match flag {
1586 TcpFlag::Syn => "SYN",
1587 TcpFlag::Ack => "ACK",
1588 TcpFlag::Fin => "FIN",
1589 TcpFlag::Rst => "RST",
1590 TcpFlag::Psh => "PSH",
1591 TcpFlag::Urg => "URG",
1592 TcpFlag::FinRst => "FIN,RST",
1593 TcpFlag::All => "ALL",
1594 TcpFlag::None => "NONE",
1595 }
1596}
1597
1598fn rate_unit_to_str(unit: &RateUnit) -> &'static str {
1600 match unit {
1601 RateUnit::Second => "/second",
1602 RateUnit::Minute => "/minute",
1603 RateUnit::Hour => "/hour",
1604 RateUnit::Day => "/day",
1605 }
1606}
1607
1608fn connection_state_to_str(state: &ConnectionState) -> &'static str {
1610 match state {
1611 ConnectionState::New => "NEW",
1612 ConnectionState::Established => "ESTABLISHED",
1613 ConnectionState::Related => "RELATED",
1614 ConnectionState::Invalid => "INVALID",
1615 ConnectionState::Untracked => "UNTRACKED",
1616 }
1617}
1618
1619fn owner_match_to_str(owner: &OwnerMatch) -> String {
1621 match owner {
1622 OwnerMatch::UidOwner(uid) => format!("--uid-owner {}", uid),
1623 OwnerMatch::GidOwner(gid) => format!("--gid-owner {}", gid),
1624 }
1625}
1626
1627fn build_rule_args_from_new_structures(rule: &IptablesRule) -> String {
1629 let mut parts: Vec<String> = Vec::new();
1630
1631 let (criteria, target) = match rule {
1632 IptablesRule::Raw {
1633 criteria, target, ..
1634 }
1635 | IptablesRule::Filter {
1636 criteria, target, ..
1637 }
1638 | IptablesRule::Nat {
1639 criteria, target, ..
1640 }
1641 | IptablesRule::Mangle {
1642 criteria, target, ..
1643 }
1644 | IptablesRule::Security {
1645 criteria, target, ..
1646 } => (criteria, target),
1647 };
1648
1649 parts.push(format!("-p {}", protocol_to_str(&criteria.protocol)));
1651
1652 if let Some(ref src) = criteria.source {
1654 let prefix = if src.inverted { "! -s " } else { "-s " };
1655 parts.push(format!("{} {}", prefix, src.value.to_string()));
1656 }
1657
1658 if let Some(ref dst) = criteria.destination {
1660 let prefix = if dst.inverted { "! -d " } else { "-d " };
1661 parts.push(format!("{} {}", prefix, dst.value.to_string()));
1662 }
1663
1664 if let Some(ref iface) = criteria.network_interface_in {
1666 let prefix = if iface.inverted { "! -i " } else { "-i " };
1667 parts.push(format!("{} {}", prefix, iface.value));
1668 }
1669
1670 if let Some(ref iface) = criteria.network_interface_out {
1672 let prefix = if iface.inverted { "! -o " } else { "-o " };
1673 parts.push(format!("{} {}", prefix, iface.value));
1674 }
1675
1676 if let Some(sport) = get_source_port(&criteria.protocol) {
1678 parts.push(format!("--sport {}", port_spec_to_str(sport)));
1679 }
1680
1681 if let Some(dport) = get_dest_port(&criteria.protocol) {
1683 parts.push(format!("--dport {}", port_spec_to_str(dport)));
1684 }
1685
1686 if let Some(tcp_flags) = get_tcp_flags(&criteria.protocol) {
1688 let mask_strs: Vec<&str> = tcp_flags.mask.iter().map(|f| tcp_flag_to_str(f)).collect();
1689 let comp_strs: Vec<&str> = tcp_flags.comp.iter().map(|f| tcp_flag_to_str(f)).collect();
1690 parts.push(format!(
1691 "-m tcp --tcp-flags {} {}",
1692 mask_strs.join(","),
1693 comp_strs.join(",")
1694 ));
1695 }
1696
1697 if let Some(ref conntrack) = criteria.conntrack {
1699 let prefix = if conntrack.inverted {
1700 "! -m conntrack --ctstate "
1701 } else {
1702 "-m conntrack --ctstate "
1703 };
1704 let states: Vec<&str> = conntrack
1705 .value
1706 .states
1707 .iter()
1708 .map(|s| connection_state_to_str(s))
1709 .collect();
1710 parts.push(format!("{} {}", prefix, states.join(",")));
1711 }
1712
1713 if let Some(ref limit) = criteria.limit {
1715 let rate_str = format!("{} {}", limit.rate, rate_unit_to_str(&limit.unit));
1716 let mut limit_part = format!("-m limit --limit {}", rate_str);
1717 if let Some(ref burst) = limit.burst {
1718 limit_part.push_str(&format!(" --limit-burst {}", burst));
1719 }
1720 parts.push(limit_part);
1721 }
1722
1723 if let Some(ref fragment) = criteria.fragment
1725 && *fragment
1726 {
1727 parts.push("-f".to_string());
1728 }
1729 if let Some(ref icmp_type) = get_icmp_type(&criteria.protocol) {
1734 parts.push(format!("--icmp-type {}", icmp_type));
1735 }
1736 if let Some(ref icmp_code) = get_icmp_code(&criteria.protocol) {
1737 parts.push(format!("--icmp-code {}", icmp_code));
1738 }
1739
1740 if let Some(ref owner) = criteria.owner {
1742 let prefix = if owner.inverted {
1743 "! -m owner "
1744 } else {
1745 "-m owner "
1746 };
1747 parts.push(format!("{} {}", prefix, owner_match_to_str(&owner.value)));
1748 }
1749
1750 if let Some(ref comment) = criteria.comment {
1752 parts.push(format!("-m comment --comment '{}'", comment));
1753 }
1754
1755 parts.push(target_to_str(target));
1757
1758 parts.join(" ")
1759}
1760
1761fn get_source_port(proto: &Protocol) -> Option<&PortSpec> {
1763 match proto {
1764 Protocol::Tcp {
1765 source_port: Some(p),
1766 ..
1767 }
1768 | Protocol::Udp {
1769 source_port: Some(p),
1770 ..
1771 } => Some(p),
1772 _ => None,
1773 }
1774}
1775
1776fn get_dest_port(proto: &Protocol) -> Option<&PortSpec> {
1777 match proto {
1778 Protocol::Tcp {
1779 dest_port: Some(p), ..
1780 }
1781 | Protocol::Udp {
1782 dest_port: Some(p), ..
1783 } => Some(p),
1784 _ => None,
1785 }
1786}
1787
1788fn get_tcp_flags(proto: &Protocol) -> Option<&TcpFlagsMatch> {
1789 match proto {
1790 Protocol::Tcp {
1791 tcp_flags: Some(f), ..
1792 } => Some(f),
1793 _ => None,
1794 }
1795}
1796
1797fn get_icmp_type(proto: &Protocol) -> Option<u8> {
1798 match proto {
1799 Protocol::Icmp {
1800 icmp_type: Some(t), ..
1801 } => Some(*t),
1802 _ => None,
1803 }
1804}
1805
1806fn get_icmp_code(proto: &Protocol) -> Option<u8> {
1807 match proto {
1808 Protocol::Icmp {
1809 icmp_code: Some(c), ..
1810 } => Some(*c),
1811 _ => None,
1812 }
1813}
1814
1815fn target_to_str(target: &IptablesTarget) -> String {
1817 match target {
1818 IptablesTarget::Accept => "-j ACCEPT".to_string(),
1819 IptablesTarget::Drop => "-j DROP".to_string(),
1820 IptablesTarget::Reject {
1821 with: Some(reject_with),
1822 } => {
1823 format!("-j REJECT --reject-with {}", reject_with)
1824 }
1825 IptablesTarget::Reject { with: None } => "-j REJECT".to_string(),
1826 IptablesTarget::Log {
1827 prefix: Some(p),
1828 level: Some(l),
1829 } => {
1830 format!("-j LOG --log-prefix '{}' --log-level {}", p, l)
1831 }
1832 IptablesTarget::Log {
1833 prefix: Some(p),
1834 level: None,
1835 } => {
1836 format!("-j LOG --log-prefix '{}'", p)
1837 }
1838 IptablesTarget::Log {
1839 prefix: None,
1840 level: Some(l),
1841 } => {
1842 format!("-j LOG --log-level {}", l)
1843 }
1844 IptablesTarget::Log {
1845 prefix: None,
1846 level: None,
1847 } => "-j LOG".to_string(),
1848 IptablesTarget::Return => "-j RETURN".to_string(),
1849 IptablesTarget::Jump(target) => format!("-j {}", target),
1850 IptablesTarget::Goto(target) => format!("-g {}", target),
1851 IptablesTarget::Custom(target) => format!("-j {}", target),
1852 }
1853}
1854
1855#[cfg(test)]
1858mod tests {
1859 use super::*;
1860
1861 #[test]
1862 fn test_deserialize_filter_tcp_present_append() {
1863 let yaml = r#"
1864IpVersion: V4
1865State: Present
1866Action: Append
1867Table: Filter
1868Details:
1869 Target: Accept
1870 Chain: Input
1871 Criteria:
1872 Protocol:
1873 Tcp:
1874 SourcePort: 80
1875 DestPort: 443
1876 TcpFlags:
1877 Mask: [Syn, Ack]
1878 Comp: [Syn]
1879 Source:
1880 Value: "192.168.1.0/24"
1881 Inverted: false
1882 Comment: "Allow HTTPS web traffic"
1883 "#;
1884
1885 let state: IptablesExpectedState = yaml_serde::from_str(yaml).unwrap();
1886
1887 match state {
1888 IptablesExpectedState::Present {
1889 ip_version,
1890 action,
1891 rule,
1892 } => {
1893 assert_eq!(ip_version, IpVersion::V4);
1894 assert_eq!(action, IptablesInsertionAction::Append);
1895 match rule {
1896 IptablesRule::Filter {
1897 chain,
1898 criteria,
1899 target,
1900 } => {
1901 assert_eq!(chain, FilterChain::Input);
1902 assert_eq!(target, IptablesTarget::Accept);
1903 assert!(criteria.comment.is_some());
1904 }
1905 _ => panic!("Expected Filter rule"),
1906 }
1907 }
1908 _ => panic!("Expected Present state"),
1909 }
1910 }
1911
1912 #[test]
1913 fn test_deserialize_nat_udp_absent() {
1914 let yaml = r#"
1915State: Absent
1916IpVersion: V6
1917Table: Nat
1918Details:
1919 Chain: Postrouting
1920 Criteria:
1921 Protocol:
1922 Udp:
1923 SourcePort: 53
1924 DestPort:
1925 Start: 1024
1926 End: 65535
1927 Destination:
1928 Value: "2001:db8::/32"
1929 Inverted: true
1930 Target:
1931 Reject:
1932 With: IcmpPortUnreachable
1933 "#;
1934
1935 let state: IptablesExpectedState = yaml_serde::from_str(yaml).unwrap();
1936
1937 match state {
1938 IptablesExpectedState::Absent { ip_version, rule } => {
1939 assert_eq!(ip_version, IpVersion::V6);
1940 match rule {
1941 IptablesRule::Nat {
1942 chain,
1943 criteria,
1944 target,
1945 } => {
1946 assert_eq!(chain, NatChain::Postrouting);
1947 assert_eq!(
1948 target,
1949 IptablesTarget::Reject {
1950 with: Some(RejectWith::IcmpPortUnreachable)
1951 }
1952 );
1953 if let Protocol::Udp { dest_port, .. } = criteria.protocol {
1954 assert_eq!(
1955 dest_port,
1956 Some(PortSpec::Range {
1957 start: 1024,
1958 end: 65535
1959 })
1960 );
1961 } else {
1962 panic!("Expected UDP protocol");
1963 }
1964 }
1965 _ => panic!("Expected Nat rule"),
1966 }
1967 }
1968 _ => panic!("Expected Absent state"),
1969 }
1970 }
1971
1972 #[test]
1973 fn test_deserialize_mangle_conntrack_owner() {
1974 let yaml = r#"
1975State: Present
1976Action: Append
1977Table: Mangle
1978Details:
1979 Chain: Output
1980 Criteria:
1981 Protocol: All
1982 Conntrack:
1983 Value:
1984 States: [New, Established]
1985 Inverted: false
1986 Owner:
1987 Value:
1988 UidOwner: "www-data"
1989 Inverted: true
1990 Target:
1991 Log:
1992 Prefix: "LOG_MARK: "
1993 Level: 4
1994 "#;
1995
1996 let state: IptablesExpectedState = yaml_serde::from_str(yaml).unwrap();
1997
1998 match state {
1999 IptablesExpectedState::Present { rule, .. } => match rule {
2000 IptablesRule::Mangle { chain, target, .. } => {
2001 assert_eq!(chain, MangleChain::Output);
2002 assert_eq!(
2003 target,
2004 IptablesTarget::Log {
2005 prefix: Some("LOG_MARK: ".to_string()),
2006 level: Some(4),
2007 }
2008 );
2009 }
2010 _ => panic!("Expected Mangle rule"),
2011 },
2012 _ => panic!("Expected Present state"),
2013 }
2014 }
2015
2016 #[test]
2017 fn test_yaml_vs_rust_api_equivalence_ssh() {
2018 let yaml = r#"
2020State: Present
2021IpVersion: V4
2022Action: Append
2023Table: Filter
2024Details:
2025 Chain: Input
2026 Criteria:
2027 Protocol:
2028 Tcp:
2029 DestPort: 22
2030 Target: Accept
2031"#;
2032 let yaml_state: IptablesExpectedState = yaml_serde::from_str(yaml).unwrap();
2033
2034 let rust_state = IptablesExpectedState::allow_ssh(None);
2036
2037 assert_eq!(yaml_state, rust_state);
2038 }
2039
2040 #[test]
2041 fn test_yaml_vs_rust_api_equivalence_http() {
2042 let yaml = r#"
2044State: Present
2045IpVersion: V4
2046Action: Append
2047Table: Filter
2048Details:
2049 Chain: Input
2050 Criteria:
2051 Protocol:
2052 Tcp:
2053 DestPort: 80
2054 Target: Accept
2055"#;
2056 let yaml_state: IptablesExpectedState = yaml_serde::from_str(yaml).unwrap();
2057
2058 let rust_state = IptablesExpectedState::allow_tcp_port(80, None);
2060
2061 assert_eq!(yaml_state, rust_state);
2062 }
2063
2064 #[test]
2065 fn test_yaml_vs_rust_api_equivalence_drop_all() {
2066 let yaml = r#"
2068State: Present
2069IpVersion: V4
2070Action: Append
2071Table: Filter
2072Details:
2073 Chain: Input
2074 Criteria:
2075 Protocol: All
2076 Target: Drop
2077"#;
2078 let yaml_state: IptablesExpectedState = yaml_serde::from_str(yaml).unwrap();
2079
2080 let rust_state = IptablesExpectedState::drop_all_incoming(None);
2082
2083 assert_eq!(yaml_state, rust_state);
2084 }
2085
2086 #[test]
2087 fn test_yaml_vs_rust_api_equivalence_udp() {
2088 let yaml = r#"
2090State: Present
2091IpVersion: V4
2092Action: Append
2093Table: Filter
2094Details:
2095 Chain: Input
2096 Criteria:
2097 Protocol:
2098 Udp:
2099 DestPort: 53
2100 Target: Accept
2101"#;
2102 let yaml_state: IptablesExpectedState = yaml_serde::from_str(yaml).unwrap();
2103
2104 let rust_state = IptablesExpectedState::allow_udp_port(53, None);
2106
2107 assert_eq!(yaml_state, rust_state);
2108 }
2109
2110 #[test]
2111 fn test_yaml_vs_rust_api_equivalence_complex_rule() {
2112 let yaml = r#"
2114State: Present
2115IpVersion: V4
2116Action:
2117 Insert:
2118 Position: 1
2119Table: Filter
2120Details:
2121 Chain: Input
2122 Criteria:
2123 Protocol:
2124 Tcp:
2125 DestPort: 443
2126 Source:
2127 Value: "192.168.1.0/24"
2128 Inverted: false
2129 Target: Accept
2130"#;
2131 let yaml_state: IptablesExpectedState = yaml_serde::from_str(yaml).unwrap();
2132
2133 let rust_state = IptablesExpectedState::present(
2135 IpVersion::V4,
2136 IptablesInsertionAction::Insert { position: 1 },
2137 IptablesRule::Filter {
2138 chain: FilterChain::Input,
2139 criteria: MatchCriteria {
2140 protocol: Protocol::Tcp {
2141 source_port: None,
2142 dest_port: Some(PortSpec::Single(443)),
2143 tcp_flags: None,
2144 },
2145 source: Some(Invert::new(CidrBlock::parse("192.168.1.0/24").unwrap())),
2146 destination: None,
2147 network_interface_in: None,
2148 network_interface_out: None,
2149 fragment: None,
2150 limit: None,
2151 conntrack: None,
2152 owner: None,
2153 comment: None,
2154 },
2155 target: IptablesTarget::Accept,
2156 },
2157 );
2158
2159 assert_eq!(yaml_state, rust_state);
2160 }
2161
2162 fn sample_rule() -> IptablesRule {
2163 IptablesRule::Filter {
2164 chain: FilterChain::Input,
2165 criteria: MatchCriteria {
2166 protocol: Protocol::Udp {
2167 source_port: None,
2168 dest_port: Some(PortSpec::Single(123)),
2169 },
2170 source: None,
2171 destination: None,
2172 network_interface_in: None,
2173 network_interface_out: None,
2174 fragment: None,
2175 limit: None,
2176 conntrack: None,
2177 owner: None,
2178 comment: None,
2179 },
2180 target: IptablesTarget::Accept,
2181 }
2182 }
2183
2184 #[test]
2185 fn named_constructors_match_explicit_form() {
2186 assert_eq!(
2187 IptablesExpectedState::appended(sample_rule()),
2188 IptablesExpectedState::present(
2189 IpVersion::V4,
2190 IptablesInsertionAction::Append,
2191 sample_rule()
2192 )
2193 );
2194 assert_eq!(
2195 IptablesExpectedState::inserted_at(3, sample_rule()),
2196 IptablesExpectedState::present(
2197 IpVersion::V4,
2198 IptablesInsertionAction::Insert { position: 3 },
2199 sample_rule()
2200 )
2201 );
2202 assert_eq!(
2203 IptablesExpectedState::removed(sample_rule()),
2204 IptablesExpectedState::absent(IpVersion::V4, sample_rule())
2205 );
2206 }
2207
2208 #[test]
2209 fn for_ipv6_matches_explicit_form() {
2210 assert_eq!(
2211 IptablesExpectedState::inserted_at(3, sample_rule()).for_ipv6(),
2212 IptablesExpectedState::present(
2213 IpVersion::V6,
2214 IptablesInsertionAction::Insert { position: 3 },
2215 sample_rule()
2216 )
2217 );
2218 assert_eq!(
2219 IptablesExpectedState::removed(sample_rule()).for_ipv6(),
2220 IptablesExpectedState::absent(IpVersion::V6, sample_rule())
2221 );
2222 assert_eq!(
2223 IptablesExpectedState::allow_tcp_port(80, Some(1)).for_ipv6(),
2224 IptablesExpectedState::allow_tcp_port_with_ip(80, IpVersion::V6, Some(1))
2225 );
2226 assert_eq!(
2227 IptablesExpectedState::allow_udp_port(53, None).for_ipv6(),
2228 IptablesExpectedState::allow_udp_port_with_ip(53, IpVersion::V6, None)
2229 );
2230 assert_eq!(
2231 IptablesExpectedState::allow_ssh(None).for_ipv6(),
2232 IptablesExpectedState::allow_ssh_with_ip(IpVersion::V6, None)
2233 );
2234 assert_eq!(
2235 IptablesExpectedState::drop_all_incoming(Some(2)).for_ipv6(),
2236 IptablesExpectedState::drop_all_incoming_with_ip(IpVersion::V6, Some(2))
2237 );
2238 }
2239}