1use std::collections::{HashMap, HashSet};
10use std::net::IpAddr;
11use std::sync::{Arc, RwLock};
12
13use tokio_util::sync::CancellationToken;
14
15use koi_common::integration;
16use koi_common::types::{ServiceRecord, META_QUERY};
17
18pub struct CertmeshBridge {
21 core: Arc<koi_certmesh::CertmeshCore>,
22}
23
24impl CertmeshBridge {
25 pub fn new(core: Arc<koi_certmesh::CertmeshCore>) -> Arc<Self> {
26 Arc::new(Self { core })
27 }
28}
29
30impl integration::CertmeshSnapshot for CertmeshBridge {
31 fn active_members(&self) -> Vec<integration::MemberSummary> {
32 let roster_path = self.core.paths().roster_path();
33 let Ok(roster) = koi_certmesh::roster::load_roster(&roster_path) else {
34 return Vec::new();
35 };
36 roster
37 .members
38 .into_iter()
39 .filter(|m| m.status == koi_certmesh::roster::MemberStatus::Active)
40 .map(|m| integration::MemberSummary {
41 hostname: m.hostname,
42 sans: m.cert_sans,
43 cert_expires: Some(m.cert_expires),
44 last_seen: m.last_seen,
45 status: "active".to_string(),
46 proxy_entries: m
47 .proxy_entries
48 .into_iter()
49 .map(|p| integration::ProxyConfigSummary {
50 name: p.name,
51 listen_port: p.listen_port,
52 backend: p.backend,
53 allow_remote: p.allow_remote,
54 })
55 .collect(),
56 })
57 .collect()
58 }
59}
60
61pub struct MdnsBridge {
66 records: Arc<RwLock<HashMap<String, HashMap<String, ServiceRecord>>>>,
67 cancel: CancellationToken,
68}
69
70impl MdnsBridge {
71 pub async fn spawn(core: Arc<koi_mdns::MdnsCore>) -> Arc<Self> {
73 let records = Arc::new(RwLock::new(HashMap::new()));
74 let cancel = CancellationToken::new();
75
76 let meta_core = Arc::clone(&core);
77 let meta_records = Arc::clone(&records);
78 let meta_cancel = cancel.clone();
79 tokio::spawn(async move {
80 if let Ok(handle) = meta_core.subscribe_type(META_QUERY).await {
81 run_meta_browse(meta_core, handle, meta_records, meta_cancel).await;
82 }
83 });
84
85 Arc::new(Self { records, cancel })
86 }
87
88 fn snapshot_records(&self) -> Vec<ServiceRecord> {
89 let guard = self.records.read().unwrap_or_else(|e| e.into_inner());
90 guard
91 .values()
92 .flat_map(|map| map.values().cloned())
93 .collect()
94 }
95}
96
97impl Drop for MdnsBridge {
98 fn drop(&mut self) {
99 self.cancel.cancel();
100 }
101}
102
103impl integration::MdnsSnapshot for MdnsBridge {
104 fn host_ips(&self) -> HashMap<String, IpAddr> {
105 let records = self.snapshot_records();
106 let mut map = HashMap::new();
107 for record in &records {
108 let Some(host) = record.host.as_deref() else {
109 continue;
110 };
111 let Some(ip) = record.ip.as_deref().and_then(|ip| ip.parse().ok()) else {
112 continue;
113 };
114 let hostname = host.trim_end_matches('.').trim_end_matches(".local");
115 if !hostname.is_empty() {
116 map.insert(hostname.to_string(), ip);
117 }
118 }
119 map
120 }
121
122 fn cached_records(&self) -> Vec<ServiceRecord> {
123 self.snapshot_records()
124 }
125}
126
127pub struct DnsBridge {
130 runtime: Arc<koi_dns::DnsRuntime>,
131}
132
133impl DnsBridge {
134 pub fn new(runtime: Arc<koi_dns::DnsRuntime>) -> Arc<Self> {
135 Arc::new(Self { runtime })
136 }
137}
138
139impl integration::DnsProbe for DnsBridge {
140 fn resolve_local(&self, name: &str) -> Option<Vec<IpAddr>> {
141 use hickory_proto::rr::RecordType;
142 let core = self.runtime.core();
143 let result = core
144 .resolve_local(name, RecordType::A)
145 .or_else(|| core.resolve_local(name, RecordType::AAAA));
146 result.map(|r| r.ips)
147 }
148}
149
150pub struct AcmeDnsBridge {
156 runtime: Arc<koi_dns::DnsRuntime>,
157}
158
159impl AcmeDnsBridge {
160 pub fn new(runtime: Arc<koi_dns::DnsRuntime>) -> Arc<Self> {
161 Arc::new(Self { runtime })
162 }
163}
164
165impl integration::AcmeDnsSolver for AcmeDnsBridge {
166 fn set_txt(&self, name: &str, value: &str) {
167 self.runtime.core().add_txt(name, value);
168 }
169
170 fn clear_txt(&self, name: &str) {
171 self.runtime.core().remove_txt(name);
172 }
173
174 fn get_txt(&self, name: &str) -> Vec<String> {
175 self.runtime.core().get_txt(name)
176 }
177}
178
179pub struct ProxyBridge {
182 _core: Arc<koi_proxy::ProxyCore>,
183}
184
185impl ProxyBridge {
186 pub fn new(core: Arc<koi_proxy::ProxyCore>) -> Arc<Self> {
187 Arc::new(Self { _core: core })
188 }
189}
190
191impl integration::ProxySnapshot for ProxyBridge {
192 fn entries(&self) -> Vec<integration::ProxyEntrySummary> {
193 let Ok(entries) = koi_proxy::config::load_entries() else {
195 return Vec::new();
196 };
197 entries
198 .into_iter()
199 .map(|e| integration::ProxyEntrySummary {
200 name: e.name,
201 listen_port: e.listen_port,
202 backend: e.backend,
203 })
204 .collect()
205 }
206}
207
208pub struct AliasFeedbackBridge {
211 core: Arc<koi_certmesh::CertmeshCore>,
212}
213
214impl AliasFeedbackBridge {
215 pub fn new(core: Arc<koi_certmesh::CertmeshCore>) -> Arc<Self> {
216 Arc::new(Self { core })
217 }
218}
219
220impl integration::AliasFeedback for AliasFeedbackBridge {
221 fn record_alias(&self, hostname: &str, alias: &str) {
222 let core = Arc::clone(&self.core);
223 let hostname = hostname.to_string();
224 let alias = alias.to_string();
225 tokio::spawn(async move {
227 let _ = core.add_alias_sans(&hostname, &[alias]).await;
228 });
229 }
230}
231
232async fn run_meta_browse(
235 core: Arc<koi_mdns::MdnsCore>,
236 handle: koi_mdns::BrowseSubscription,
237 records: Arc<RwLock<HashMap<String, HashMap<String, ServiceRecord>>>>,
238 cancel: CancellationToken,
239) {
240 let mut seen = HashSet::<String>::new();
244 loop {
245 tokio::select! {
246 _ = cancel.cancelled() => break,
247 event = handle.recv() => {
248 let Some(event) = event else { break; };
249 if let koi_mdns::events::MdnsEvent::Found(record) = event {
250 let service_type = record.name;
251 if seen.insert(service_type.clone()) {
252 let c = Arc::clone(&core);
253 let r = Arc::clone(&records);
254 let t = service_type.clone();
255 let cancel_child = cancel.clone();
256 tokio::spawn(async move {
257 if let Ok(handle) = c.subscribe_type(&t).await {
258 run_type_browse(handle, r, cancel_child).await;
259 }
260 });
261 }
262 }
263 }
264 }
265 }
266}
267
268async fn run_type_browse(
269 handle: koi_mdns::BrowseSubscription,
270 records: Arc<RwLock<HashMap<String, HashMap<String, ServiceRecord>>>>,
271 cancel: CancellationToken,
272) {
273 loop {
274 tokio::select! {
275 _ = cancel.cancelled() => break,
276 event = handle.recv() => {
277 let Some(event) = event else { break; };
278 match event {
279 koi_mdns::events::MdnsEvent::Resolved(record) => {
280 let mut guard = records.write().unwrap_or_else(|e| e.into_inner());
281 let entry = guard.entry(record.service_type.clone()).or_default();
282 entry.insert(record.name.clone(), record);
283 }
284 koi_mdns::events::MdnsEvent::Removed { name, service_type } => {
287 if service_type.is_empty() {
288 continue;
289 }
290 let mut guard = records.write().unwrap_or_else(|e| e.into_inner());
291 if let Some(map) = guard.get_mut(&service_type) {
292 map.remove(&name);
293 }
294 }
295 _ => {}
296 }
297 }
298 }
299 }
300}