1use super::wire::*;
3use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, ParameterDefinition, StartupExamples,
6};
7use crate::protocol::{EventType, SpawnContext};
8use crate::server::dns::actions::DnsProtocol;
9use crate::state::AppState;
10use anyhow::{bail, Result};
11use serde_json::{json, Value};
12use std::sync::LazyLock;
13
14pub static DOQ_QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
15 EventType::new(
16 "doq_query",
17 "DNS query on a dedicated QUIC stream",
18 json!({"type":"send_dns_a_response","domain":"example.com","ip":"192.0.2.1","query_id":0}),
19 )
20 .with_parameters(vec![
21 crate::llm::actions::Parameter {
22 name: "domain".into(),
23 type_hint: "string".into(),
24 description: "Queried DNS name".into(),
25 required: true,
26 },
27 crate::llm::actions::Parameter {
28 name: "query_type".into(),
29 type_hint: "string".into(),
30 description: "DNS record type".into(),
31 required: true,
32 },
33 crate::llm::actions::Parameter {
34 name: "query_id".into(),
35 type_hint: "number".into(),
36 description: "Always zero on DoQ".into(),
37 required: true,
38 },
39 crate::llm::actions::Parameter {
40 name: "stream_id".into(),
41 type_hint: "number".into(),
42 description: "QUIC stream identifying this transaction".into(),
43 required: true,
44 },
45 crate::llm::actions::Parameter {
46 name: "peer_addr".into(),
47 type_hint: "string".into(),
48 description: "Remote IP and UDP port".into(),
49 required: true,
50 },
51 ])
52 .with_actions(DoqProtocol::new().get_sync_actions())
53});
54
55#[derive(Default)]
56pub struct DoqProtocol;
57impl DoqProtocol {
58 pub fn new() -> Self {
59 Self
60 }
61}
62impl Protocol for DoqProtocol {
63 fn protocol_name(&self) -> &'static str {
64 "DoQ"
65 }
66 fn stack_name(&self) -> &'static str {
67 "ETH>IP>UDP>QUIC>DNS"
68 }
69 fn description(&self) -> &'static str {
70 "DNS over QUIC (RFC 9250), with independent query streams and authenticated TLS"
71 }
72 fn keywords(&self) -> Vec<&'static str> {
73 vec!["doq", "dns-over-quic", "dns over quic"]
74 }
75 fn group_name(&self) -> &'static str {
76 "DNS"
77 }
78 fn example_prompt(&self) -> &'static str {
79 "Serve DNS over QUIC on UDP 8853, answering example.com with 192.0.2.1"
80 }
81 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
82 vec![]
83 }
84 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
85 DnsProtocol::query_actions()
86 .into_iter()
87 .filter(|a| a.name != "send_dns_response")
88 .collect()
89 }
90 fn get_event_types(&self) -> Vec<EventType> {
91 vec![DOQ_QUERY_EVENT.clone()]
92 }
93 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
94 vec![
95 parameter(
96 "cert_path",
97 "PEM certificate chain; requires key_path",
98 json!("server.pem"),
99 None,
100 ),
101 parameter(
102 "key_path",
103 "PEM private key; requires cert_path",
104 json!("server-key.pem"),
105 None,
106 ),
107 parameter(
108 "handshake_timeout_secs",
109 "QUIC handshake deadline, 1..60 seconds",
110 json!(10),
111 Some(json!(HANDSHAKE_TIMEOUT.as_secs())),
112 ),
113 parameter(
114 "exchange_timeout_secs",
115 "Whole query/handler/write deadline, 1..300 seconds",
116 json!(30),
117 Some(json!(EXCHANGE_TIMEOUT.as_secs())),
118 ),
119 parameter(
120 "idle_timeout_secs",
121 "QUIC idle timeout, 1..3600 seconds",
122 json!(300),
123 Some(json!(IDLE_TIMEOUT.as_secs())),
124 ),
125 parameter(
126 "max_connections",
127 "Maximum simultaneous connections including handshakes, 1..256",
128 json!(64),
129 Some(json!(MAX_CONNECTIONS)),
130 ),
131 parameter(
132 "max_streams",
133 "Maximum active query streams per connection, 1..32",
134 json!(32),
135 Some(json!(MAX_STREAMS)),
136 ),
137 ]
138 }
139 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
140 use crate::protocol::metadata::{
141 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
142 };
143 ProtocolMetadataV2::builder().state(DevelopmentState::Experimental)
144 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(853)).well_known_udp_port(853)
145 .max_inbound_bytes(super::wire::MAX_FRAME_BYTES)
146 .implementation("quinn 0.11, rustls 0.23 and existing hickory-proto 0.24 DNS codec")
147 .llm_control("Typed A/AAAA/MX/TXT/CNAME/NXDOMAIN responses per QUIC query stream")
148 .e2e_testing("Loopback framing, bounds, certificate validation, command/event and cleanup tests; independent-peer evidence recorded in tests/server/doq/AGENTS.md")
149 .notes("Single-question QUERY only; no AXFR/IXFR, DNSSEC signing, recursive resolver, 0-RTT, automatic padding or server-initiated messages. PEM certificate/key supported; generated localhost certificate otherwise.")
150 .build()
151 }
152 fn get_startup_examples(&self) -> StartupExamples {
153 StartupExamples::new(
154 json!({"type":"open_server","base_stack":"doq","port":8853,"instruction":"Answer A queries for example.com with 192.0.2.1"}),
155 json!({"type":"open_server","base_stack":"doq","port":8853,"event_handlers":[{"event_pattern":"doq_query","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_a_response','query_id':0,'domain':event['domain'],'ip':'192.0.2.1'}])"}}]}),
156 json!({"type":"open_server","base_stack":"doq","port":8853,"event_handlers":[{"event_pattern":"doq_query","handler":{"type":"static","actions":[{"type":"ignore_query"}]}}]}),
157 )
158 }
159}
160impl Server for DoqProtocol {
161 fn spawn(
162 &self,
163 ctx: SpawnContext,
164 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
165 {
166 Box::pin(super::DoqServer::spawn(ctx))
167 }
168 fn execute_action(&self, mut action: Value) -> Result<ActionResult> {
169 let name = action.get("type").and_then(Value::as_str).unwrap_or("");
170 if !self.get_sync_actions().iter().any(|a| a.name == name) {
171 bail!("Unknown DoQ action: {name}");
172 }
173 if let Some(obj) = action.as_object_mut() {
175 obj.insert("query_id".into(), json!(0));
176 }
177 DnsProtocol::new().execute_action(action)
178 }
179}