Skip to main content

netget/client/quic/
actions.rs

1//! Raw QUIC FIN-delimited stream exchanges, independent from HTTP/3.
2use 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}