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 zone.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 let zone = zone.trim_end_matches('.');
106
107 if record_part == zone {
108 return (zone.to_string(), "_unknown".to_string(), "_tcp".to_string());
109 }
110
111 let parts: Vec<&str> = record_part.split('.').collect();
112
113 let service = if !parts.is_empty() && parts[0].starts_with('_') {
114 parts[0][1..].to_string()
115 } else {
116 "_unknown".to_string()
117 };
118
119 let proto = if parts.len() >= 2 && parts[1].starts_with('_') {
120 parts[1][1..].to_string()
121 } else {
122 "_tcp".to_string()
123 };
124
125 let remaining = if parts.len() > 2 {
126 parts[2..].join(".")
127 } else {
128 "@".to_string()
129 };
130
131 (remaining, service, proto)
132}
133
134pub fn parse_tlsa_name(fqdn: &str, zone: &str) -> (String, u32, String) {
136 let record_part = strip_zone(fqdn, zone);
137 let zone = zone.trim_end_matches('.');
138
139 if record_part == zone {
140 return (zone.to_string(), 0, "tcp".to_string());
141 }
142
143 let parts: Vec<&str> = record_part.split('.').collect();
144
145 let port: u32 = if !parts.is_empty() && parts[0].starts_with('_') {
146 parts[0][1..].parse().unwrap_or(0)
147 } else {
148 0
149 };
150
151 let proto = if parts.len() >= 2 && parts[1].starts_with('_') {
152 parts[1][1..].to_string()
153 } else {
154 "tcp".to_string()
155 };
156
157 let remaining = if parts.len() > 2 {
158 parts[2..].join(".")
159 } else {
160 "@".to_string()
161 };
162
163 (remaining, port, proto)
164}
165
166pub fn parse_txt_data(raw: &str) -> String {
168 let mut result = String::new();
169
170 for cap in TXT.captures_iter(raw) {
171 result.push_str(&cap[1]);
172 }
173
174 result
175}
176
177pub fn can_proxy(rtype: &str) -> bool {
179 matches!(rtype, "A" | "AAAA" | "CNAME")
180}