Skip to main content

netget/server/doq/
actions.rs

1//! Structured DNS response actions, delegated to the existing DNS codec.
2use 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        // The transport owns correlation. A model-supplied nonzero ID must never leak to DoQ.
174        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}