Skip to main content

netget/client/dot/
actions.rs

1//! DoT (DNS over TLS) client protocol actions implementation
2
3use 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
14/// DoT client connected event
15pub static DOT_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "dot_connected",
18        "DoT client connected to DNS-over-TLS server",
19        json!({"type": "send_dns_query", "domain": "example.com", "query_type": "A"}),
20    )
21    .with_parameters(vec![Parameter {
22        name: "remote_addr".to_string(),
23        type_hint: "string".to_string(),
24        description: "DNS-over-TLS server address".to_string(),
25        required: true,
26    }])
27});
28
29/// DoT client response received event
30pub static DOT_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31    EventType::new(
32        "dot_response_received",
33        "DNS response received from DoT server",
34        json!({"type": "send_dns_query", "domain": "mail.example.com", "query_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: "response_code".to_string(),
45            type_hint: "string".to_string(),
46            description: "DNS response code (NOERROR, NXDOMAIN, etc.)".to_string(),
47            required: true,
48        },
49        Parameter {
50            name: "answers".to_string(),
51            type_hint: "array".to_string(),
52            description: "DNS answer records".to_string(),
53            required: true,
54        },
55        Parameter {
56            name: "authorities".to_string(),
57            type_hint: "array".to_string(),
58            description: "DNS authority records".to_string(),
59            required: true,
60        },
61        Parameter {
62            name: "additionals".to_string(),
63            type_hint: "array".to_string(),
64            description: "DNS additional records".to_string(),
65            required: true,
66        },
67    ])
68});
69
70/// DoT client protocol action handler
71pub struct DotClientProtocol;
72
73impl DotClientProtocol {
74    pub fn new() -> Self {
75        Self
76    }
77}
78
79// Implement Protocol trait (common functionality)
80impl Protocol for DotClientProtocol {
81    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
82        vec![
83            ParameterDefinition {
84                name: "verify_tls".to_string(),
85                description: "Verify TLS certificate (default: true)".to_string(),
86                type_hint: "boolean".to_string(),
87                required: false,
88                example: json!(true),
89                default: None,
90            },
91            ParameterDefinition {
92                name: "server_name".to_string(),
93                description:
94                    "Server name for TLS SNI (optional, defaults to hostname from address)"
95                        .to_string(),
96                type_hint: "string".to_string(),
97                required: false,
98                example: json!("dns.google"),
99                default: None,
100            },
101        ]
102    }
103    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
104        vec![
105            ActionDefinition {
106                name: "send_dns_query".to_string(),
107                description: "Send a DNS query to the DoT server".to_string(),
108                parameters: vec![
109                    Parameter {
110                        name: "domain".to_string(),
111                        type_hint: "string".to_string(),
112                        description: "Domain name to query".to_string(),
113                        required: true,
114                    },
115                    Parameter {
116                        name: "query_type".to_string(),
117                        type_hint: "string".to_string(),
118                        description: "DNS query type (A, AAAA, MX, TXT, CNAME, NS, etc.)"
119                            .to_string(),
120                        required: true,
121                    },
122                    Parameter {
123                        name: "recursive".to_string(),
124                        type_hint: "boolean".to_string(),
125                        description: "Request recursive resolution (default: true)".to_string(),
126                        required: false,
127                    },
128                ],
129                example: json!({
130                    "type": "send_dns_query",
131                    "domain": "example.com",
132                    "query_type": "A",
133                    "recursive": true
134                }),
135                log_template: None,
136            },
137            ActionDefinition {
138                name: "disconnect".to_string(),
139                description: "Disconnect from the DoT server".to_string(),
140                parameters: vec![],
141                example: json!({
142                    "type": "disconnect"
143                }),
144                log_template: None,
145            },
146        ]
147    }
148    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
149        vec![
150            ActionDefinition {
151                name: "send_dns_query".to_string(),
152                description: "Send another DNS query in response to received data".to_string(),
153                parameters: vec![
154                    Parameter {
155                        name: "domain".to_string(),
156                        type_hint: "string".to_string(),
157                        description: "Domain name to query".to_string(),
158                        required: true,
159                    },
160                    Parameter {
161                        name: "query_type".to_string(),
162                        type_hint: "string".to_string(),
163                        description: "DNS query type".to_string(),
164                        required: true,
165                    },
166                    Parameter {
167                        name: "recursive".to_string(),
168                        type_hint: "boolean".to_string(),
169                        description: "Request recursive resolution".to_string(),
170                        required: false,
171                    },
172                ],
173                example: json!({
174                    "type": "send_dns_query",
175                    "domain": "example.org",
176                    "query_type": "AAAA"
177                }),
178                log_template: None,
179            },
180            ActionDefinition {
181                name: "wait_for_more".to_string(),
182                description: "Wait for more DNS responses without sending a query".to_string(),
183                parameters: vec![],
184                example: json!({
185                    "type": "wait_for_more"
186                }),
187                log_template: None,
188            },
189        ]
190    }
191    fn protocol_name(&self) -> &'static str {
192        "DoT"
193    }
194    fn get_event_types(&self) -> Vec<EventType> {
195        vec![
196            EventType::new(
197                "dot_connected",
198                "Triggered when DoT client connects to server",
199                json!({"type": "placeholder", "event_id": "dot_connected"}),
200            ),
201            EventType::new(
202                "dot_response_received",
203                "Triggered when DoT client receives a DNS response",
204                json!({"type": "placeholder", "event_id": "dot_response_received"}),
205            ),
206        ]
207    }
208    fn stack_name(&self) -> &'static str {
209        "ETH>IP>TCP>TLS>DNS"
210    }
211    fn keywords(&self) -> Vec<&'static str> {
212        vec![
213            "dot",
214            "dns over tls",
215            "dns-over-tls",
216            "dns tls",
217            "secure dns",
218        ]
219    }
220    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
221        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
222
223        ProtocolMetadataV2::builder()
224            .state(DevelopmentState::Experimental)
225            .implementation("hickory-dns with rustls for TLS transport")
226            .llm_control("Full control over DNS queries (domain, type, recursive flag)")
227            .e2e_testing("Public DoT servers (dns.google:853, 1.1.1.1:853)")
228            .build()
229    }
230    fn description(&self) -> &'static str {
231        "DoT (DNS over TLS) client for secure DNS queries"
232    }
233    fn example_prompt(&self) -> &'static str {
234        "Connect to dns.google:853 and query example.com A record"
235    }
236    fn group_name(&self) -> &'static str {
237        "DNS"
238    }
239
240    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
241        use crate::llm::actions::StartupExamples;
242        use serde_json::json;
243
244        StartupExamples::new(
245            // LLM mode: LLM controls DNS queries over TLS
246            json!({
247                "type": "open_client",
248                "remote_addr": "dns.google:853",
249                "base_stack": "dot",
250                "instruction": "Query example.com for A and AAAA records, then report the IP addresses"
251            }),
252            // Script mode: Code-based deterministic responses
253            json!({
254                "type": "open_client",
255                "remote_addr": "dns.google:853",
256                "base_stack": "dot",
257                "event_handlers": [{
258                    "event_pattern": "dot_response_received",
259                    "handler": {
260                        "type": "script",
261                        "language": "python",
262                        "code": "<dot_client_handler>"
263                    }
264                }]
265            }),
266            // Static mode: Fixed DNS query on connect
267            json!({
268                "type": "open_client",
269                "remote_addr": "dns.google:853",
270                "base_stack": "dot",
271                "event_handlers": [
272                    {
273                        "event_pattern": "dot_connected",
274                        "handler": {
275                            "type": "static",
276                            "actions": [{
277                                "type": "send_dns_query",
278                                "domain": "example.com",
279                                "query_type": "A"
280                            }]
281                        }
282                    },
283                    {
284                        "event_pattern": "dot_response_received",
285                        "handler": {
286                            "type": "static",
287                            "actions": [{
288                                "type": "disconnect"
289                            }]
290                        }
291                    }
292                ]
293            }),
294        )
295    }
296}
297
298// Implement Client trait (client-specific functionality)
299impl Client for DotClientProtocol {
300    fn connect(
301        &self,
302        ctx: crate::protocol::ConnectContext,
303    ) -> std::pin::Pin<
304        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
305    > {
306        Box::pin(async move {
307            use crate::client::dot::DotClient;
308            DotClient::connect_with_llm_actions(
309                ctx.remote_addr,
310                ctx.llm_client,
311                ctx.state,
312                ctx.status_tx,
313                ctx.client_id,
314                ctx.startup_params,
315            )
316            .await
317        })
318    }
319    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
320        let action_type = action
321            .get("type")
322            .and_then(|v| v.as_str())
323            .context("Missing 'type' field in action")?;
324
325        match action_type {
326            "send_dns_query" => {
327                let domain = action
328                    .get("domain")
329                    .and_then(|v| v.as_str())
330                    .context("Missing 'domain' field")?
331                    .to_string();
332
333                let query_type = action
334                    .get("query_type")
335                    .and_then(|v| v.as_str())
336                    .context("Missing 'query_type' field")?
337                    .to_string();
338
339                let recursive = action
340                    .get("recursive")
341                    .and_then(|v| v.as_bool())
342                    .unwrap_or(true);
343
344                // Return custom result with query data
345                Ok(ClientActionResult::Custom {
346                    name: "dns_query".to_string(),
347                    data: json!({
348                        "domain": domain,
349                        "query_type": query_type,
350                        "recursive": recursive,
351                    }),
352                })
353            }
354            "disconnect" => Ok(ClientActionResult::Disconnect),
355            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
356            _ => Err(anyhow::anyhow!(
357                "Unknown DoT client action: {}",
358                action_type
359            )),
360        }
361    }
362}