Skip to main content

netget/client/doq/
actions.rs

1//! DoQ uses the existing typed DNS query vocabulary and its own transport events.
2use crate::client::dns::actions::{
3    DnsClientProtocol, DNS_CLIENT_CONNECTED_EVENT, DNS_CLIENT_RESPONSE_RECEIVED_EVENT,
4};
5use crate::llm::actions::{
6    client_trait::{Client, ClientActionResult},
7    protocol_trait::Protocol,
8    ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
9};
10use crate::protocol::log_template::LogTemplate;
11use crate::protocol::{ConnectContext, EventType};
12use crate::server::doq::wire::*;
13use crate::state::AppState;
14use anyhow::Result;
15use serde_json::{json, Value};
16use std::sync::LazyLock;
17
18// Reuse the DNS action vocabulary, with descriptions and logs for the DoQ transport.
19fn doq_actions(mut actions: Vec<ActionDefinition>) -> Vec<ActionDefinition> {
20    for action in &mut actions {
21        match action.name.as_str() {
22            "send_dns_query" => {
23                if let Some(parameter) = action
24                    .parameters
25                    .iter_mut()
26                    .find(|parameter| parameter.name == "query_type")
27                {
28                    parameter.description = "DNS record type to query, such as A, AAAA, MX or TXT. AXFR and IXFR are not supported.".into();
29                }
30                action.log_template = Some(
31                    LogTemplate::new()
32                        .with_info("-> DoQ {domain} {query_type}")
33                        .with_debug("DoQ send_dns_query: {domain} {query_type} recursion_desired={recursion_desired}"),
34                );
35            }
36            "disconnect" => {
37                action.log_template = Some(LogTemplate::new().with_info("-> DoQ disconnect"));
38            }
39            "wait_for_more" => {
40                action.log_template = Some(LogTemplate::new().with_info("DoQ wait for more data"));
41            }
42            _ => {}
43        }
44    }
45    actions
46}
47
48pub static DOQ_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
49    EventType::new(
50        "doq_connected",
51        "Authenticated DoQ connection established",
52        json!({"type":"send_dns_query","domain":"example.com","query_type":"A"}),
53    )
54    .with_parameters(DNS_CLIENT_CONNECTED_EVENT.parameters.clone())
55    .with_actions(DoqClientProtocol::new().get_sync_actions())
56});
57pub static DOQ_RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
58    EventType::new(
59        "doq_response_received",
60        "Complete DNS response on a QUIC query stream",
61        json!({"type":"wait_for_more"}),
62    )
63    .with_parameters(DNS_CLIENT_RESPONSE_RECEIVED_EVENT.parameters.clone())
64    .with_actions(DoqClientProtocol::new().get_sync_actions())
65});
66pub static DOQ_ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
67    EventType::new(
68        "doq_query_error",
69        "A DoQ transaction failed or timed out",
70        json!({"type":"wait_for_more"}),
71    )
72    .with_parameters(vec![
73        Parameter {
74            name: "domain".into(),
75            type_hint: "string".into(),
76            description: "The DNS name that could not be queried, such as example.com.".into(),
77            required: true,
78        },
79        Parameter {
80            name: "query_type".into(),
81            type_hint: "string".into(),
82            description: "Queried DNS record type".into(),
83            required: true,
84        },
85        Parameter {
86            name: "error".into(),
87            type_hint: "string".into(),
88            description: "Local query failure description".into(),
89            required: true,
90        },
91    ])
92    .with_actions(DoqClientProtocol::new().get_sync_actions())
93});
94
95#[derive(Default)]
96pub struct DoqClientProtocol;
97impl DoqClientProtocol {
98    pub fn new() -> Self {
99        Self
100    }
101}
102impl Protocol for DoqClientProtocol {
103    fn protocol_name(&self) -> &'static str {
104        "DoQ"
105    }
106    fn stack_name(&self) -> &'static str {
107        "ETH>IP>UDP>QUIC>DNS"
108    }
109    fn description(&self) -> &'static str {
110        "DNS over QUIC client with certificate validation and reusable concurrent query streams"
111    }
112    fn keywords(&self) -> Vec<&'static str> {
113        vec!["doq", "dns-over-quic", "dns over quic"]
114    }
115    fn group_name(&self) -> &'static str {
116        "DNS"
117    }
118    fn example_prompt(&self) -> &'static str {
119        "Connect to dns.example:853 over DoQ and query the AAAA records for example.com"
120    }
121    fn get_async_actions(&self, state: &AppState) -> Vec<ActionDefinition> {
122        doq_actions(DnsClientProtocol::new().get_async_actions(state))
123    }
124    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
125        doq_actions(DnsClientProtocol::new().get_sync_actions())
126    }
127    fn get_event_types(&self) -> Vec<EventType> {
128        vec![
129            DOQ_CONNECTED_EVENT.clone(),
130            DOQ_RESPONSE_EVENT.clone(),
131            DOQ_ERROR_EVENT.clone(),
132        ]
133    }
134    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
135        vec![
136            parameter(
137                "server_name",
138                "TLS authentication name and SNI; defaults to the address hostname",
139                json!("dns.example"),
140                None,
141            ),
142            parameter(
143                "ca_cert_path",
144                "Additional trusted PEM CA or self-signed server certificate",
145                json!("ca.pem"),
146                None,
147            ),
148            parameter(
149                "handshake_timeout_secs",
150                "Resolution and TLS handshake deadline, 1..60 seconds",
151                json!(10),
152                Some(json!(HANDSHAKE_TIMEOUT.as_secs())),
153            ),
154            parameter(
155                "exchange_timeout_secs",
156                "Whole stream query/response deadline, 1..300 seconds",
157                json!(30),
158                Some(json!(EXCHANGE_TIMEOUT.as_secs())),
159            ),
160            parameter(
161                "idle_timeout_secs",
162                "QUIC idle timeout, 1..3600 seconds",
163                json!(300),
164                Some(json!(IDLE_TIMEOUT.as_secs())),
165            ),
166        ]
167    }
168    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
169        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
170        ProtocolMetadataV2::builder().state(DevelopmentState::Experimental)
171            .implementation("quinn/rustls with hickory-proto DNS messages; public roots plus optional PEM trust")
172            .llm_control("Typed DNS queries, response/error events, bounded follow-up actions and disconnect")
173            .e2e_testing("Loopback TLS/framing, injected commands, event follow-ups, cancellation and shutdown; independent-server evidence recorded in tests/client/doq/AGENTS.md")
174            .notes("Certificate and hostname validation always enabled. Single-question ordinary queries only; no AXFR/IXFR, automatic reconnect, 0-RTT or transport fallback. Maximum 32 active exchanges and four queries per automatic action chain.")
175            .build()
176    }
177    fn get_startup_examples(&self) -> StartupExamples {
178        StartupExamples::new(
179            json!({"type":"open_client","protocol":"doq","remote_addr":"dns.example:853","instruction":"Query A records for example.com"}),
180            json!({"type":"open_client","protocol":"doq","remote_addr":"dns.example:853","event_handlers":[{"event_pattern":"doq_connected","handler":{"type":"script","language":"python","code":"import json,sys\nevent=json.load(sys.stdin)['event']\ndef respond(actions):\n    print(json.dumps({'actions':actions}))\nrespond([{'type':'send_dns_query','domain':'example.com','query_type':'A'}])"}}]}),
181            json!({"type":"open_client","protocol":"doq","remote_addr":"dns.example:853","event_handlers":[{"event_pattern":"doq_connected","handler":{"type":"static","actions":[{"type":"send_dns_query","domain":"example.com","query_type":"A"}]}},{"event_pattern":"doq_response_received","handler":{"type":"static","actions":[{"type":"disconnect"}]}}]}),
182        )
183    }
184}
185impl Client for DoqClientProtocol {
186    fn connect(
187        &self,
188        ctx: ConnectContext,
189    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
190    {
191        Box::pin(super::DoqClient::connect(ctx))
192    }
193    fn execute_action(&self, action: Value) -> Result<ClientActionResult> {
194        let result = DnsClientProtocol::new().execute_action(action)?;
195        if let ClientActionResult::Custom { name, data } = &result {
196            if name == "dns_query" {
197                super::build_query(data)?;
198            }
199        }
200        Ok(result)
201    }
202}