1use 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
18fn 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}