lab_ops/cmd/
dns_parser.rs1use std::sync::LazyLock;
4
5use regex::Regex;
6
7pub static SOA: LazyLock<Regex> =
9 LazyLock::new(|| Regex::new(r"^\S+\s+\d+\s+IN\s+SOA\s+").unwrap());
10pub static ZONE: LazyLock<Regex> =
12 LazyLock::new(|| Regex::new(r"^(\S+)\s+(\d+)\s+IN\s+(\S+)\s+(.*)$").unwrap());
13pub static PROXIED: LazyLock<Regex> =
15 LazyLock::new(|| Regex::new(r"\s*;\s*cf_tags=cf-proxied:(true|false)\s*$").unwrap());
16pub static TXT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#""([^"]*)""#).unwrap());
18
19#[derive(Debug, Clone)]
21pub struct DnsRecord {
22 pub name: String,
24 pub ttl: u32,
26 pub rtype: String,
28 pub data: String,
30 pub proxied: Option<bool>,
32}
33
34pub fn parse_zone(content: &str) -> Vec<DnsRecord> {
38 let mut records = Vec::new();
39
40 for line in content.lines() {
41 let line = line.trim();
42 if line.is_empty() || line.starts_with(';') {
43 continue;
44 }
45
46 let caps = match ZONE.captures(line) {
47 Some(c) => c,
48 None => continue,
49 };
50
51 let name = caps[1].to_string();
52 let ttl: u32 = caps[2].parse().unwrap_or(1);
53 let rtype = caps[3].to_uppercase();
54 if rtype == "SOA" {
55 continue;
56 }
57
58 let raw_data = caps[4].to_string();
59
60 let (data, proxied) = split_data_and_proxied(&raw_data);
61
62 records.push(DnsRecord {
63 name,
64 ttl,
65 rtype,
66 data,
67 proxied,
68 });
69 }
70
71 records
72}
73
74pub fn split_data_and_proxied(raw: &str) -> (String, Option<bool>) {
76 let proxied = PROXIED.captures(raw).map(|c| c[1].to_lowercase() == "true");
77 let data = PROXIED.replace(raw, "").trim().to_string();
78
79 (data, proxied)
80}
81
82pub fn strip_zone(fqdn: &str, zone: &str) -> String {
86 let fqdn = fqdn.trim_end_matches('.');
87 let zone = zone.trim_end_matches('.');
88
89 if fqdn == zone {
90 return "@".to_string();
91 }
92
93 let suffix = format!(".{zone}");
94 if fqdn.to_lowercase().ends_with(&suffix.to_lowercase()) {
95 let end = fqdn.len() - suffix.len();
96 return fqdn[..end].to_string();
97 }
98
99 fqdn.to_string()
100}
101
102pub fn parse_srv_name(fqdn: &str, zone: &str) -> (String, String, String) {
104 let record_part = strip_zone(fqdn, zone);
105
106 if record_part == "@" {
107 return ("@".to_string(), "_unknown".to_string(), "_tcp".to_string());
108 }
109
110 let parts: Vec<&str> = record_part.split('.').collect();
111
112 let service = if !parts.is_empty() && parts[0].starts_with('_') {
113 parts[0][1..].to_string()
114 } else {
115 "_unknown".to_string()
116 };
117
118 let proto = if parts.len() >= 2 && parts[1].starts_with('_') {
119 parts[1][1..].to_string()
120 } else {
121 "_tcp".to_string()
122 };
123
124 let remaining = if parts.len() > 2 {
125 parts[2..].join(".")
126 } else {
127 "@".to_string()
128 };
129
130 (remaining, service, proto)
131}
132
133pub fn parse_tlsa_name(fqdn: &str, zone: &str) -> (String, u32, String) {
135 let record_part = strip_zone(fqdn, zone);
136
137 if record_part == "@" {
138 return ("@".to_string(), 0, "tcp".to_string());
139 }
140
141 let parts: Vec<&str> = record_part.split('.').collect();
142
143 let port: u32 = if !parts.is_empty() && parts[0].starts_with('_') {
144 parts[0][1..].parse().unwrap_or(0)
145 } else {
146 0
147 };
148
149 let proto = if parts.len() >= 2 && parts[1].starts_with('_') {
150 parts[1][1..].to_string()
151 } else {
152 "tcp".to_string()
153 };
154
155 let remaining = if parts.len() > 2 {
156 parts[2..].join(".")
157 } else {
158 "@".to_string()
159 };
160
161 (remaining, port, proto)
162}
163
164pub fn parse_txt_data(raw: &str) -> String {
166 let mut result = String::new();
167
168 for cap in TXT.captures_iter(raw) {
169 result.push_str(&cap[1]);
170 }
171
172 result
173}
174
175pub fn can_proxy(rtype: &str) -> bool {
177 matches!(rtype, "A" | "AAAA" | "CNAME")
178}