1use crate::llm::actions::{
3 client_trait::{Client, ClientActionResult},
4 protocol_trait::Protocol,
5 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::{ConnectContext, EventType};
9use crate::state::AppState;
10use anyhow::{bail, Result};
11use serde_json::{json, Value};
12use std::sync::LazyLock;
13
14fn field(name: &str, hint: &str, description: &str) -> Parameter {
15 Parameter {
16 name: name.into(),
17 type_hint: hint.into(),
18 description: description.into(),
19 required: true,
20 }
21}
22pub static QUIC_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
23 EventType::new(
24 "quic_connected",
25 "Authenticated raw QUIC connection established",
26 json!({"type":"send_quic_data","data":"hello"}),
27 )
28 .with_parameters(vec![
29 field(
30 "remote_addr",
31 "string",
32 "Authenticated QUIC peer's socket address",
33 ),
34 field(
35 "max_datagram_size",
36 "number",
37 "Largest DATAGRAM payload the server accepts, or null when it does not support \
38 RFC 9221 datagrams",
39 ),
40 ])
41 .with_actions(QuicClientProtocol.get_sync_actions())
42});
43pub static QUIC_RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
44 EventType::new(
45 "quic_data_received",
46 "Complete FIN-terminated raw QUIC response",
47 json!({"type":"wait_for_more"}),
48 )
49 .with_parameters(vec![
50 field("stream_id", "number", "QUIC wire stream ID"),
51 field("data", "string", "Received payload"),
52 field("encoding", "string", "utf8 or hex payload encoding"),
53 field("bytes", "number", "Payload byte count"),
54 ])
55 .with_actions(QuicClientProtocol.get_sync_actions())
56});
57pub static QUIC_DATAGRAM_EVENT: LazyLock<EventType> = LazyLock::new(|| {
58 EventType::new(
59 "quic_datagram_received",
60 "An unreliable QUIC DATAGRAM frame (RFC 9221) arrived from the server",
61 json!({"type":"wait_for_more"}),
62 )
63 .with_parameters(vec![
64 field(
65 "data",
66 "string",
67 "Datagram payload, read according to encoding",
68 ),
69 field(
70 "encoding",
71 "string",
72 "utf8 for printable text, otherwise hex",
73 ),
74 field("bytes", "number", "Payload byte count"),
75 ])
76 .with_actions(QuicClientProtocol.get_sync_actions())
77});
78pub static QUIC_ERROR_EVENT: LazyLock<EventType> = LazyLock::new(|| {
79 EventType::new(
80 "quic_stream_error",
81 "Raw QUIC stream exchange failed",
82 json!({"type":"wait_for_more"}),
83 )
84 .with_parameters(vec![field("error", "string", "Local failure description")])
85 .with_actions(QuicClientProtocol.get_sync_actions())
86});
87#[derive(Default)]
88pub struct QuicClientProtocol;
89impl QuicClientProtocol {
90 pub fn new() -> Self {
91 Self
92 }
93}
94impl Protocol for QuicClientProtocol {
95 fn protocol_name(&self) -> &'static str {
96 "QUIC"
97 }
98 fn stack_name(&self) -> &'static str {
99 "ETH>IP>UDP>QUIC"
100 }
101 fn group_name(&self) -> &'static str {
102 "Core"
103 }
104 fn description(&self) -> &'static str {
105 "Raw QUIC client with authenticated concurrent bidirectional stream exchanges"
106 }
107 fn keywords(&self) -> Vec<&'static str> {
108 vec!["quic", "raw quic", "quic streams"]
109 }
110 fn example_prompt(&self) -> &'static str {
111 "Connect to localhost:4433 over raw QUIC and send hello"
112 }
113 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
114 let mut p = crate::utils::quic::client_parameters();
115 p.push(crate::utils::quic::parameter(
116 "alpn",
117 "Raw stream application protocol (default netget-quic)",
118 json!("netget-quic"),
119 Some(json!("netget-quic")),
120 ));
121 p
122 }
123 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
124 let mut a = crate::server::quic::actions::QuicProtocol.get_sync_actions();
125 a.retain(|a| a.name != "close_this_stream");
126 a[0].description="Open a new bidirectional QUIC stream, send raw data and FIN, and receive at most 1 MiB until peer FIN. Every action uses an independent stream. Encodings: utf8, hex, base64.".into();
127 if let Some(d) = a.iter_mut().find(|a| a.name == "send_quic_datagram") {
128 d.description = "Send an unreliable RFC 9221 DATAGRAM frame to the server: no \
129 stream, no retransmission, at most the connection's max_datagram_size bytes \
130 (from quic_connected). Any reply arrives as quic_datagram_received."
131 .into();
132 }
133 a.push(ActionDefinition {
134 name: "disconnect".into(),
135 description: "Close the QUIC connection and cancel all streams".into(),
136 parameters: vec![],
137 example: json!({"type":"disconnect"}),
138 log_template: Some(
139 LogTemplate::new()
140 .with_info("Disconnect raw QUIC peer and cancel all active streams"),
141 ),
142 });
143 a
144 }
145 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
146 self.get_sync_actions()
147 }
148 fn get_event_types(&self) -> Vec<EventType> {
149 vec![
150 QUIC_CONNECTED_EVENT.clone(),
151 QUIC_RESPONSE_EVENT.clone(),
152 QUIC_ERROR_EVENT.clone(),
153 QUIC_DATAGRAM_EVENT.clone(),
154 ]
155 }
156 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
157 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
158 ProtocolMetadataV2::builder().state(DevelopmentState::Experimental).implementation("quinn 0.11/rustls; raw netget-quic ALPN, distinct from HTTP/3")
159 .llm_control("Explicit text/hex/base64 stream payloads, response/error events, bounded follow-ups and disconnect")
160 .e2e_testing("Independent aioquic 1.3.0 echo server; authenticated binary exchanges, multiplexing, injection/follow-ups and cleanup")
161 .notes("Each send_quic_data opens one FIN-delimited bidirectional exchange; send_quic_datagram sends an RFC 9221 DATAGRAM and incoming datagrams arrive as quic_datagram_received. No append to existing streams, incoming server streams, 0-RTT, reconnect or application framing. Certificates and hostname always verified; custom PEM trust supported. 1 MiB per direction, 32 exchanges, four queries per automatic chain.").build()
162 }
163 fn get_startup_examples(&self) -> StartupExamples {
164 StartupExamples::new(
165 json!({"type":"open_client","protocol":"quic","remote_addr":"localhost:4433","instruction":"Send hello on a new QUIC stream"}),
166 json!({"type":"open_client","protocol":"quic","remote_addr":"localhost:4433","event_handlers":[{"event_pattern":"quic_connected","handler":{"type":"script","language":"python","code":"import json\nprint(json.dumps({'actions':[{'type':'send_quic_data','data':'hello'}]}))"}}]}),
167 json!({"type":"open_client","protocol":"quic","remote_addr":"localhost:4433","event_handlers":[{"event_pattern":"quic_connected","handler":{"type":"static","actions":[{"type":"send_quic_data","data":"hello"}]}},{"event_pattern":"*","handler":{"type":"static","actions":[]}}]}),
168 )
169 }
170}
171impl Client for QuicClientProtocol {
172 fn connect(
173 &self,
174 ctx: ConnectContext,
175 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
176 {
177 Box::pin(super::QuicClient::connect(ctx))
178 }
179 fn execute_action(&self, action: Value) -> Result<ClientActionResult> {
180 match action["type"].as_str() {
181 Some("send_quic_data") => {
182 super::payload(&action)?;
183 Ok(ClientActionResult::Custom {
184 name: "quic_exchange".into(),
185 data: action,
186 })
187 }
188 Some("send_quic_datagram") => Ok(ClientActionResult::Custom {
189 name: crate::server::quic::actions::DATAGRAM_RESULT.into(),
190 data: json!(hex::encode(crate::server::quic::actions::datagram_payload(
191 &action
192 )?)),
193 }),
194 Some("wait_for_more") => Ok(ClientActionResult::WaitForMore),
195 Some("disconnect") => Ok(ClientActionResult::Disconnect),
196 _ => bail!("Unknown QUIC client action"),
197 }
198 }
199}