auth dns over atproto
1use std::net::IpAddr;
2use std::path::Path;
3
4use serde::Deserialize;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum ConfigError {
9 #[error("io error: {0}")]
10 Io(#[from] std::io::Error),
11 #[error("toml parse error: {0}")]
12 Toml(#[from] toml::de::Error),
13}
14
15/// Top-level config covering all onis services.
16///
17/// Load from a TOML file with [`OnisConfig::load`]. Every field has a default,
18/// so an empty (or missing) file produces a usable config.
19#[derive(Debug, Deserialize)]
20#[serde(default)]
21pub struct OnisConfig {
22 /// Configuration for the appview service.
23 pub appview: AppviewConfig,
24 /// Configuration for the DNS server.
25 pub dns: DnsConfig,
26 /// Configuration for the verification service.
27 pub verify: VerifyConfig,
28}
29
30impl OnisConfig {
31 /// Load config from `ONIS_CONFIG` env var path, or `onis.toml` in the
32 /// current directory. Returns defaults if the file does not exist.
33 pub fn load() -> Result<Self, ConfigError> {
34 let path = std::env::var("ONIS_CONFIG").unwrap_or_else(|_| "onis.toml".to_string());
35 let path = Path::new(&path);
36
37 if path.exists() {
38 let content = std::fs::read_to_string(path)?;
39 Ok(toml::from_str(&content)?)
40 } else {
41 Ok(Self::default())
42 }
43 }
44}
45
46impl Default for OnisConfig {
47 fn default() -> Self {
48 Self {
49 appview: AppviewConfig::default(),
50 dns: DnsConfig::default(),
51 verify: VerifyConfig::default(),
52 }
53 }
54}
55
56#[derive(Debug, Deserialize)]
57#[serde(default)]
58pub struct AppviewConfig {
59 /// Address and port for the appview HTTP server.
60 pub bind: String,
61 /// WebSocket URL for the TAP firehose.
62 pub tap_url: String,
63 /// Whether to acknowledge TAP messages.
64 pub tap_acks: bool,
65 /// Seconds to wait before reconnecting after a TAP connection error.
66 pub tap_reconnect_delay: u64,
67 /// Path to the shared zone index SQLite database.
68 pub index_path: String,
69 /// Directory for per-DID SQLite databases.
70 pub db_dir: String,
71 /// Database pool configuration.
72 pub database: DatabaseConfig,
73}
74
75impl Default for AppviewConfig {
76 fn default() -> Self {
77 Self {
78 bind: "0.0.0.0:3000".to_string(),
79 tap_url: "ws://localhost:2480/channel".to_string(),
80 tap_acks: true,
81 tap_reconnect_delay: 5,
82 index_path: "./data/index.db".to_string(),
83 db_dir: "./data/dbs".to_string(),
84 database: DatabaseConfig::default(),
85 }
86 }
87}
88
89#[derive(Debug, Clone, Deserialize)]
90#[serde(default)]
91pub struct DatabaseConfig {
92 /// Seconds to wait when the database is locked.
93 pub busy_timeout: u64,
94 /// Max connections for per-user database pools.
95 pub user_max_connections: u32,
96 /// Max connections for the shared index database pool.
97 pub index_max_connections: u32,
98}
99
100impl Default for DatabaseConfig {
101 fn default() -> Self {
102 Self {
103 busy_timeout: 5,
104 user_max_connections: 5,
105 index_max_connections: 10,
106 }
107 }
108}
109
110#[derive(Debug, Deserialize)]
111#[serde(default)]
112pub struct DnsConfig {
113 /// URL of the appview API.
114 pub appview_url: String,
115 /// Address for the DNS server to listen on.
116 pub bind: String,
117 /// Port for the DNS server.
118 pub port: u16,
119 /// Seconds before a TCP connection times out.
120 pub tcp_timeout: u64,
121 /// Minimum TTL enforced on all DNS responses.
122 pub ttl_floor: u32,
123 /// Log a warning for queries slower than this (milliseconds).
124 pub slow_query_threshold_ms: u64,
125 /// SOA record defaults for zones without a user-published SOA.
126 pub soa: SoaConfig,
127 /// NS records to serve for all zones (fully qualified, trailing dot).
128 pub ns: Vec<String>,
129 /// Bind address for the metrics HTTP server (e.g. "0.0.0.0:9100").
130 pub metrics_bind: String,
131}
132
133impl Default for DnsConfig {
134 fn default() -> Self {
135 Self {
136 appview_url: "http://localhost:3000".to_string(),
137 bind: "0.0.0.0".to_string(),
138 port: 5353,
139 tcp_timeout: 30,
140 ttl_floor: 60,
141 slow_query_threshold_ms: 50,
142 soa: SoaConfig::default(),
143 ns: vec![
144 "ns1.example.com.".to_string(),
145 "ns2.example.com.".to_string(),
146 ],
147 metrics_bind: "0.0.0.0:9100".to_string(),
148 }
149 }
150}
151
152#[derive(Debug, Deserialize)]
153#[serde(default)]
154pub struct SoaConfig {
155 /// SOA record TTL in seconds.
156 pub ttl: u32,
157 /// SOA refresh interval in seconds.
158 pub refresh: i32,
159 /// SOA retry interval in seconds.
160 pub retry: i32,
161 /// SOA expire interval in seconds.
162 pub expire: i32,
163 /// SOA minimum (negative cache) TTL in seconds.
164 pub minimum: u32,
165 /// SOA MNAME (primary nameserver, fully qualified).
166 pub mname: String,
167 /// SOA RNAME (admin email in DNS format, fully qualified).
168 pub rname: String,
169}
170
171impl Default for SoaConfig {
172 fn default() -> Self {
173 Self {
174 ttl: 3600,
175 refresh: 3600,
176 retry: 900,
177 expire: 604800,
178 minimum: 300,
179 mname: "ns1.example.com.".to_string(),
180 rname: "admin.example.com.".to_string(),
181 }
182 }
183}
184
185#[derive(Debug, Deserialize)]
186#[serde(default)]
187pub struct VerifyConfig {
188 /// Onis appview to call.
189 pub appview_url: String,
190 /// Address to start listening on for api.
191 pub bind: String,
192 /// Port used for api.
193 pub port: u16,
194 /// Seconds between scheduled verification runs.
195 pub check_interval: u64,
196 /// Seconds a zone must be stale before reverification.
197 pub recheck_interval: i64,
198 /// Expected NS records that indicate correct delegation.
199 pub expected_ns: Vec<String>,
200 /// Optional custom resolver IP addresses.
201 pub nameservers: Vec<String>,
202 /// Port used when resolving against custom nameservers.
203 pub dns_port: u16,
204}
205
206impl Default for VerifyConfig {
207 fn default() -> Self {
208 Self {
209 appview_url: "http://localhost:3000".to_string(),
210 bind: "0.0.0.0".to_string(),
211 port: 3001,
212 check_interval: 60,
213 recheck_interval: 3600,
214 expected_ns: vec![
215 "ns1.example.com".to_string(),
216 "ns2.example.com".to_string(),
217 ],
218 nameservers: vec![],
219 dns_port: 53,
220 }
221 }
222}
223
224impl VerifyConfig {
225 /// Parse the nameservers list into IP addresses.
226 /// Returns None if the list is empty.
227 pub fn parse_nameservers(&self) -> Result<Option<Vec<IpAddr>>, std::net::AddrParseError> {
228 if self.nameservers.is_empty() {
229 return Ok(None);
230 }
231 let addrs: Result<Vec<IpAddr>, _> = self
232 .nameservers
233 .iter()
234 .map(|s| s.parse())
235 .collect();
236 addrs.map(Some)
237 }
238}
239
240#[cfg(test)]
241#[allow(clippy::unwrap_used)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn parse_nameservers_empty_returns_none() {
247 let config = VerifyConfig::default();
248 let result = config.parse_nameservers().unwrap();
249 assert_eq!(result, None);
250 }
251
252 #[test]
253 fn parse_nameservers_valid_ipv4() {
254 let config = VerifyConfig {
255 nameservers: vec!["1.1.1.1".to_string(), "8.8.8.8".to_string()],
256 ..Default::default()
257 };
258 let result = config.parse_nameservers().unwrap().unwrap();
259 assert_eq!(result.len(), 2);
260 assert_eq!(result, vec![
261 "1.1.1.1".parse::<IpAddr>().unwrap(),
262 "8.8.8.8".parse::<IpAddr>().unwrap(),
263 ]);
264 }
265
266 #[test]
267 fn parse_nameservers_valid_ipv6() {
268 let config = VerifyConfig {
269 nameservers: vec!["2001:4860:4860::8888".to_string()],
270 ..Default::default()
271 };
272 let result = config.parse_nameservers().unwrap().unwrap();
273 assert_eq!(result.len(), 1);
274 assert_eq!(result, vec!["2001:4860:4860::8888".parse::<IpAddr>().unwrap()]);
275 }
276
277 #[test]
278 fn parse_nameservers_mixed_v4_v6() {
279 let config = VerifyConfig {
280 nameservers: vec!["1.1.1.1".to_string(), "::1".to_string()],
281 ..Default::default()
282 };
283 let result = config.parse_nameservers().unwrap().unwrap();
284 assert_eq!(result.len(), 2);
285 }
286
287 #[test]
288 fn parse_nameservers_invalid_returns_err() {
289 let config = VerifyConfig {
290 nameservers: vec!["not-an-ip".to_string()],
291 ..Default::default()
292 };
293 assert!(config.parse_nameservers().is_err());
294 }
295
296 #[test]
297 fn parse_nameservers_one_invalid_fails_all() {
298 let config = VerifyConfig {
299 nameservers: vec!["1.1.1.1".to_string(), "bad".to_string()],
300 ..Default::default()
301 };
302 assert!(config.parse_nameservers().is_err());
303 }
304}