1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static DOH_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "doh_connected",
18 "DoH client successfully connected to DNS-over-HTTPS server",
19 json!({"type": "query_dns", "domain": "example.com", "record_type": "A"}),
20 )
21 .with_parameters(vec![Parameter {
22 name: "server_url".to_string(),
23 type_hint: "string".to_string(),
24 description: "DoH server URL (e.g., https://dns.google/dns-query)".to_string(),
25 required: true,
26 }])
27});
28
29pub static DOH_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31 EventType::new(
32 "doh_response_received",
33 "DNS response received from DoH server",
34 json!({"type": "query_dns", "domain": "mail.example.com", "record_type": "MX"}),
35 )
36 .with_parameters(vec![
37 Parameter {
38 name: "query_id".to_string(),
39 type_hint: "number".to_string(),
40 description: "DNS query ID".to_string(),
41 required: true,
42 },
43 Parameter {
44 name: "domain".to_string(),
45 type_hint: "string".to_string(),
46 description: "Domain name queried".to_string(),
47 required: true,
48 },
49 Parameter {
50 name: "query_type".to_string(),
51 type_hint: "string".to_string(),
52 description: "DNS record type (A, AAAA, MX, etc.)".to_string(),
53 required: true,
54 },
55 Parameter {
56 name: "answers".to_string(),
57 type_hint: "array".to_string(),
58 description: "Array of DNS answers".to_string(),
59 required: true,
60 },
61 Parameter {
62 name: "status".to_string(),
63 type_hint: "string".to_string(),
64 description: "Response status (NoError, NXDomain, etc.)".to_string(),
65 required: true,
66 },
67 ])
68});
69
70pub struct DohClientProtocol;
72
73impl DohClientProtocol {
74 pub fn new() -> Self {
75 Self
76 }
77}
78
79impl Protocol for DohClientProtocol {
81 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
82 vec![
83 ParameterDefinition {
84 name: "ca_cert_pem".to_string(),
85 description: "PEM-encoded certificate to trust in addition to the system \
86 roots. Use this to reach a DoH server presenting a private-CA \
87 or self-signed certificate -- including NetGet's own DoH \
88 server, which serves a self-signed cert by default."
89 .to_string(),
90 type_hint: "string".to_string(),
91 required: false,
92 example: json!("-----BEGIN CERTIFICATE-----\n..."),
93 default: None,
94 },
95 ParameterDefinition {
96 name: "insecure_skip_verify".to_string(),
97 description: "Disable certificate verification entirely. Off by default, \
98 and deliberately named for what it does: it accepts ANY \
99 certificate, so a DoH connection made with it is not \
100 authenticated and offers no protection against interception. \
101 Prefer `ca_cert_pem`, which trusts one certificate rather \
102 than all of them."
103 .to_string(),
104 type_hint: "boolean".to_string(),
105 required: false,
106 example: json!(false),
107 default: None,
108 },
109 ]
110 }
111
112 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
113 vec![
114 ActionDefinition {
115 name: "query_dns".to_string(),
116 description: "Make a DNS query over HTTPS".to_string(),
117 parameters: vec![
118 Parameter {
119 name: "domain".to_string(),
120 type_hint: "string".to_string(),
121 description: "Domain name to query (e.g., example.com)".to_string(),
122 required: true,
123 },
124 Parameter {
125 name: "record_type".to_string(),
126 type_hint: "string".to_string(),
127 description:
128 "DNS record type: A, AAAA, MX, TXT, CNAME, NS, SOA, PTR, SRV, etc."
129 .to_string(),
130 required: false,
131 },
132 Parameter {
133 name: "use_get".to_string(),
134 type_hint: "boolean".to_string(),
135 description: "Use HTTP GET method instead of POST (default: false)"
136 .to_string(),
137 required: false,
138 },
139 ],
140 example: json!({
141 "type": "query_dns",
142 "domain": "example.com",
143 "record_type": "A",
144 "use_get": false
145 }),
146 log_template: None,
147 },
148 ActionDefinition {
149 name: "disconnect".to_string(),
150 description: "Disconnect from the DoH server".to_string(),
151 parameters: vec![],
152 example: json!({
153 "type": "disconnect"
154 }),
155 log_template: None,
156 },
157 ]
158 }
159 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
160 vec![
161 ActionDefinition {
162 name: "query_dns".to_string(),
163 description: "Make another DNS query in response to received data".to_string(),
164 parameters: vec![
165 Parameter {
166 name: "domain".to_string(),
167 type_hint: "string".to_string(),
168 description: "Domain name to query".to_string(),
169 required: true,
170 },
171 Parameter {
172 name: "record_type".to_string(),
173 type_hint: "string".to_string(),
174 description: "DNS record type (A, AAAA, MX, etc.)".to_string(),
175 required: false,
176 },
177 ],
178 example: json!({
179 "type": "query_dns",
180 "domain": "mail.example.com",
181 "record_type": "A"
182 }),
183 log_template: None,
184 },
185 ActionDefinition {
186 name: "wait_for_more".to_string(),
187 description: "Wait for user to trigger more queries".to_string(),
188 parameters: vec![],
189 example: json!({
190 "type": "wait_for_more"
191 }),
192 log_template: None,
193 },
194 ]
195 }
196 fn protocol_name(&self) -> &'static str {
197 "DNS-over-HTTPS"
198 }
199 fn get_event_types(&self) -> Vec<EventType> {
200 vec![
201 EventType::new(
202 "doh_connected",
203 "Triggered when DoH client connects to server",
204 json!({"type": "placeholder", "event_id": "doh_connected"}),
205 ),
206 EventType::new(
207 "doh_response_received",
208 "Triggered when DoH client receives a DNS response",
209 json!({"type": "placeholder", "event_id": "doh_response_received"}),
210 ),
211 ]
212 }
213 fn stack_name(&self) -> &'static str {
214 "ETH>IP>TCP>TLS>HTTP/2>DNS"
215 }
216 fn keywords(&self) -> Vec<&'static str> {
217 vec![
218 "doh",
219 "dns-over-https",
220 "doh client",
221 "dns client",
222 "secure dns",
223 ]
224 }
225 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
226 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
227
228 ProtocolMetadataV2::builder()
229 .state(DevelopmentState::Experimental)
230 .implementation("hickory-client with DNS-over-HTTPS support")
231 .llm_control("Full control over DNS queries with LLM-driven decision making")
232 .e2e_testing("Public DoH servers (Google, Cloudflare)")
233 .build()
234 }
235 fn description(&self) -> &'static str {
236 "DNS-over-HTTPS (DoH) client for secure DNS resolution over HTTPS (RFC 8484)"
237 }
238 fn example_prompt(&self) -> &'static str {
239 "Connect to Google DoH at https://dns.google/dns-query and resolve example.com"
240 }
241 fn group_name(&self) -> &'static str {
242 "DNS"
243 }
244
245 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
246 use crate::llm::actions::StartupExamples;
247 use serde_json::json;
248
249 StartupExamples::new(
250 json!({
252 "type": "open_client",
253 "remote_addr": "dns.google:443",
254 "base_stack": "doh",
255 "instruction": "Query example.com for all record types and summarize the DNS configuration"
256 }),
257 json!({
259 "type": "open_client",
260 "remote_addr": "dns.google:443",
261 "base_stack": "doh",
262 "event_handlers": [{
263 "event_pattern": "doh_response_received",
264 "handler": {
265 "type": "script",
266 "language": "python",
267 "code": "<doh_client_handler>"
268 }
269 }]
270 }),
271 json!({
273 "type": "open_client",
274 "remote_addr": "dns.google:443",
275 "base_stack": "doh",
276 "event_handlers": [
277 {
278 "event_pattern": "doh_connected",
279 "handler": {
280 "type": "static",
281 "actions": [{
282 "type": "query_dns",
283 "domain": "example.com",
284 "record_type": "A"
285 }]
286 }
287 },
288 {
289 "event_pattern": "doh_response_received",
290 "handler": {
291 "type": "static",
292 "actions": [{
293 "type": "disconnect"
294 }]
295 }
296 }
297 ]
298 }),
299 )
300 }
301}
302
303impl Client for DohClientProtocol {
305 fn connect(
306 &self,
307 ctx: crate::protocol::ConnectContext,
308 ) -> std::pin::Pin<
309 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
310 > {
311 Box::pin(async move {
312 use crate::client::doh::DohClient;
313 DohClient::connect_with_llm_actions(
314 ctx.remote_addr,
315 ctx.llm_client,
316 ctx.state,
317 ctx.status_tx,
318 ctx.client_id,
319 ctx.startup_params,
320 )
321 .await
322 })
323 }
324 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
325 let action_type = action
326 .get("type")
327 .and_then(|v| v.as_str())
328 .context("Missing 'type' field in action")?;
329
330 match action_type {
331 "query_dns" => {
332 let domain = action
333 .get("domain")
334 .and_then(|v| v.as_str())
335 .context("Missing 'domain' field")?
336 .to_string();
337
338 let record_type = action
339 .get("record_type")
340 .and_then(|v| v.as_str())
341 .unwrap_or("A")
342 .to_string();
343
344 let use_get = action
345 .get("use_get")
346 .and_then(|v| v.as_bool())
347 .unwrap_or(false);
348
349 Ok(ClientActionResult::Custom {
350 name: "dns_query".to_string(),
351 data: json!({
352 "domain": domain,
353 "record_type": record_type,
354 "use_get": use_get,
355 }),
356 })
357 }
358 "disconnect" => Ok(ClientActionResult::Disconnect),
359 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
360 _ => Err(anyhow::anyhow!(
361 "Unknown DoH client action: {}",
362 action_type
363 )),
364 }
365 }
366}