1use crate::llm::actions::{
6 protocol_trait::{ActionResult, Protocol, Server},
7 ActionDefinition, Parameter,
8};
9use crate::protocol::log_template::LogTemplate;
10use crate::protocol::EventType;
11use crate::server::dns::actions::DnsProtocol;
12use crate::state::app_state::AppState;
13use anyhow::Result;
14use serde_json::json;
15use std::sync::LazyLock;
16
17pub static DOT_QUERY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
20 let dns_actions = DnsProtocol::query_actions();
22
23 EventType::new(
24 "dot_query",
25 "Client sent DNS query over TLS",
26 json!({"type": "placeholder", "event_id": "dot_query"}),
27 )
28 .with_parameters(vec![
29 Parameter {
30 name: "query_id".to_string(),
31 type_hint: "number".to_string(),
32 description: "DNS query ID from the request packet".to_string(),
33 required: true,
34 },
35 Parameter {
36 name: "domain".to_string(),
37 type_hint: "string".to_string(),
38 description: "Domain name being queried".to_string(),
39 required: true,
40 },
41 Parameter {
42 name: "query_type".to_string(),
43 type_hint: "string".to_string(),
44 description: "DNS query type (A, AAAA, MX, TXT, CNAME, etc.)".to_string(),
45 required: true,
46 },
47 Parameter {
48 name: "peer_addr".to_string(),
49 type_hint: "string".to_string(),
50 description: "Client IP address and port".to_string(),
51 required: true,
52 },
53 ])
54 .with_log_template(
55 LogTemplate::new()
56 .with_info("DoT {query_type} {domain}")
57 .with_debug("DoT query from {peer_addr}: {query_type} {domain}")
58 .with_trace("DoT: {json_pretty(.)}"),
59 )
60 .with_actions(dns_actions)
61});
62
63pub struct DotProtocol {
66 dns_protocol: DnsProtocol,
67}
68
69impl DotProtocol {
70 pub fn new() -> Self {
71 Self {
72 dns_protocol: DnsProtocol::new(),
73 }
74 }
75}
76
77impl Protocol for DotProtocol {
79 fn get_async_actions(&self, state: &AppState) -> Vec<ActionDefinition> {
80 self.dns_protocol.get_async_actions(state)
81 }
82 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
83 DnsProtocol::query_actions()
84 }
85 fn protocol_name(&self) -> &'static str {
86 "DoT"
87 }
88 fn get_event_types(&self) -> Vec<EventType> {
89 vec![DOT_QUERY_EVENT.clone()]
90 }
91 fn stack_name(&self) -> &'static str {
92 "ETH>IP>TCP>TLS>DNS"
93 }
94 fn keywords(&self) -> Vec<&'static str> {
95 vec!["dot", "dns-over-tls", "dns over tls"]
96 }
97 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
98 use crate::protocol::metadata::{
99 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
100 };
101
102 ProtocolMetadataV2::builder()
103 .state(DevelopmentState::Beta)
104 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(853))
106 .well_known_port(853)
107 .implementation("hickory-proto + tokio-rustls; DNS actions and action execution are delegated to the DNS protocol")
108 .llm_control("Same as DNS (delegates to DNS protocol)")
109 .e2e_testing(
110 "TWO independent peers, neither #[ignore]d and neither able to skip. \
111 (1) tests/server/dot/real_client_test.rs drives the real kdig binary (Knot DNS \
112 3.6, a C implementation sharing no code with this tree) through `kdig +tls`: it \
113 matches the reply's transaction id against the one it chose, confirms the \
114 question section, decodes the RFC 7858 length prefix and renders the answer \
115 through its own presentation writer. Two queries for different names with \
116 different addresses, so a reply bound to the wrong question is visible. The \
117 test FAILS, naming `brew install knot`, when kdig is absent. \
118 (2) tests/server/dot/e2e_test.rs: rustls completes a real handshake and the \
119 test asserts the id and question are echoed and that each domain gets its own \
120 address. \
121 UNTIL SEPTEMBER 2026 ONLY (2) EXISTED, and its DNS half was circular: the \
122 message is hand-assembled over hickory-proto, the codec this server encodes \
123 with, so it proved our encoder agrees with our decoder. This field named kdig \
124 as the thing that would close it; kdig is now installed and does. Verified by \
125 answering with a fixed transaction id instead of the client's, at which point \
126 kdig waits out its timeout and the test fails. \
127 UNPROVEN: certificate validation of any kind (the cert is self-signed and plain \
128 `+tls` does not authenticate it), client auth, and record types beyond A.",
129 )
130 .notes("Self-signed certs only, no client auth, TLS overhead")
131 .build()
132 }
133 fn description(&self) -> &'static str {
134 "DNS-over-TLS server for secure domain resolution"
135 }
136 fn example_prompt(&self) -> &'static str {
137 "DNS-over-TLS server on port 853 resolving all queries to 93.184.216.34"
138 }
139 fn group_name(&self) -> &'static str {
140 "Core"
141 }
142 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
143 use crate::llm::actions::StartupExamples;
144 StartupExamples::new(
145 json!({
147 "type": "open_server",
148 "port": 853,
149 "base_stack": "dot",
150 "instruction": "DNS-over-TLS server resolving all A queries for example.com to 93.184.216.34, NXDOMAIN for others"
151 }),
152 json!({
154 "type": "open_server",
155 "port": 853,
156 "base_stack": "dot",
157 "event_handlers": [{
158 "event_pattern": "dot_query",
159 "handler": {
160 "type": "script",
161 "language": "python",
162 "code": "# Echo the client's transaction ID and queried name\nif event.get('domain', '').rstrip('.') == 'example.com':\n respond([{'type': 'send_dns_a_response', 'query_id': event['query_id'], 'domain': event['domain'], 'ip': '93.184.216.34', 'ttl': 300}])\nelse:\n respond([{'type': 'send_dns_nxdomain', 'query_id': event['query_id'], 'domain': event['domain'], 'query_type': event.get('query_type', 'A')}])"
163 }
164 }]
165 }),
166 json!({
172 "type": "open_server",
173 "port": 853,
174 "base_stack": "dot",
175 "event_handlers": [{
176 "event_pattern": "dot_query",
177 "handler": {
178 "type": "static",
179 "actions": [{
180 "type": "ignore_query"
181 }]
182 }
183 }]
184 }),
185 )
186 }
187}
188
189impl Server for DotProtocol {
191 fn spawn(
192 &self,
193 ctx: crate::protocol::SpawnContext,
194 ) -> std::pin::Pin<
195 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
196 > {
197 Box::pin(async move {
198 use crate::server::dot::DotServer;
199 DotServer::spawn(
203 ctx.legacy_listen_addr(),
204 ctx.llm_client,
205 ctx.state,
206 ctx.server_id,
207 ctx.status_tx,
208 )
209 .await
210 })
211 }
212 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
213 self.dns_protocol.execute_action(action)
214 }
215}