Skip to main content

netget/client/gtp/
actions.rs

1//! What the model can do as the near end of a GTP path: an SGSN towards a GGSN (GTPv1 Gn),
2//! an SGW towards a PGW (GTPv2 S5/S8), or an MME towards an SGW (GTPv2 S11) — echo the path,
3//! create a session, modify its bearer, delete it, and carry user packets through its tunnel.
4//!
5//! Every action is fields. Sequence numbers, TEIDs, F-TEIDs, the IE encoding and the inner
6//! packet's checksums are Rust's; which procedures run and with what subscriber, APN, PDN
7//! type and bearer QoS is the model's.
8use super::msg::{self, CreateParams, Role};
9use crate::llm::actions::{
10    client_trait::{Client, ClientActionResult},
11    protocol_trait::Protocol,
12    ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
13};
14use crate::protocol::log_template::LogTemplate;
15use crate::protocol::{ConnectContext, EventType};
16use crate::server::gtp::codec::{self, GtpVersion};
17use crate::state::app_state::AppState;
18use anyhow::{bail, ensure, Context, Result};
19use serde_json::{json, Value};
20use std::sync::LazyLock;
21
22#[derive(Default)]
23pub struct GtpClientProtocol;
24impl GtpClientProtocol {
25    pub fn new() -> Self {
26        Self
27    }
28}
29
30pub const ECHO: &str = "gtp_echo";
31pub const CREATE: &str = "gtp_create_session";
32pub const MODIFY: &str = "gtp_modify_bearer";
33pub const DELETE: &str = "gtp_delete_session";
34pub const GPDU: &str = "gtp_send_gpdu";
35pub const END_MARKER: &str = "gtp_send_end_marker";
36pub const ERROR_INDICATION: &str = "gtp_send_error_indication";
37pub const RESPOND: &str = "gtp_respond";
38
39fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
40    Parameter {
41        name: name.into(),
42        type_hint: type_hint.into(),
43        description: description.into(),
44        required,
45    }
46}
47
48fn action(
49    name: &str,
50    description: &str,
51    parameters: Vec<Parameter>,
52    example: Value,
53) -> ActionDefinition {
54    ActionDefinition {
55        name: name.into(),
56        description: description.into(),
57        parameters,
58        example,
59        log_template: Some(
60            LogTemplate::new().with_info(format!("-> GTP {}", name.trim_start_matches("gtp_"))),
61        ),
62    }
63}
64
65fn session_param() -> Parameter {
66    p(
67        "session",
68        "number",
69        "The session, by the number this client gave it (gtp_response's session)",
70        true,
71    )
72}
73
74fn echo() -> ActionDefinition {
75    action(
76        ECHO,
77        "Send an Echo Request on the control or user plane; the answer arrives as gtp_response (request echo_request) with the peer's Recovery counter.",
78        vec![p("plane", "string", "Which path to probe (default control)", false)
79            .with_choices(["control", "user"])],
80        json!({"type": ECHO, "plane": "control"}),
81    )
82}
83
84fn create() -> ActionDefinition {
85    action(
86        CREATE,
87        "Ask the peer for a session: Create Session Request on GTPv2 (S5/S8 or S11), Create PDP Context Request on GTPv1 (Gn). Rust allocates this side's TEIDs and adds the F-TEIDs / GSN addresses, Recovery and the sequence number. The answer arrives as gtp_response with the cause, the UE address and the peer's TEIDs, and a session number to use afterwards.",
88        vec![
89            p("imsi", "string", "Subscriber IMSI, 5 to 15 digits. An identifier you invent; nothing looks it up", true),
90            p("apn", "string", "Access point name, dotted (e.g. \"internet\")", true),
91            p("msisdn", "string", "Subscriber number, up to 15 digits", false),
92            p("mei", "string", "IMEI or IMEISV, 14 to 16 digits", false),
93            p("pdn_type", "string", "Address family asked for (default ipv4)", false)
94                .with_choices(["ipv4", "ipv6", "ipv4v6"]),
95            p("ebi", "number", "EPS bearer id / NSAPI, 5..15 (default 5)", false),
96            p("rat_type", "string", "Radio access: EUTRAN (default on GTPv2), UTRAN (default on GTPv1), GERAN, NR, LTE-M …", false),
97            p("mcc", "string", "Serving network MCC, 3 digits (default 001)", false),
98            p("mnc", "string", "Serving network MNC, 2 or 3 digits (default 01)", false),
99            p("selection_mode", "number", "APN selection mode 0..3 (default 0: subscribed APN verified)", false),
100            p("qos", "object", "Default bearer QoS: {qci 1..254 (default 9), arp_priority_level 1..15 (default 8), pre_emption_capability bool, pre_emption_vulnerability bool, mbr_uplink_kbps, mbr_downlink_kbps, gbr_uplink_kbps, gbr_downlink_kbps}. GTPv1 carries only the ARP (clamped to 1..3) in a best-effort profile", false),
101            p("ambr_uplink_kbps", "number", "APN-AMBR uplink in kbit/s (GTPv2; default 100000)", false),
102            p("ambr_downlink_kbps", "number", "APN-AMBR downlink in kbit/s (GTPv2; default 100000)", false),
103            p("request_dns", "boolean", "Ask for DNS servers in the PCO (default true)", false),
104            p("pgw_address", "string", "S11 only, required there: the PGW the SGW should create the session on", false),
105        ],
106        json!({"type": CREATE, "imsi": "001010123456789", "apn": "internet", "msisdn": "15551234567",
107               "pdn_type": "ipv4", "qos": {"qci": 9, "arp_priority_level": 8}}),
108    )
109}
110
111fn modify() -> ActionDefinition {
112    action(
113        MODIFY,
114        "Tell the peer where this side's user plane is: Modify Bearer Request on GTPv2 (on S11 this client's GTP-U endpoint is the eNodeB's S1-U F-TEID), Update PDP Context Request on GTPv1. Addressed to the peer's control TEID learned at creation.",
115        vec![session_param()],
116        json!({"type": MODIFY, "session": 1}),
117    )
118}
119
120fn delete() -> ActionDefinition {
121    action(
122        DELETE,
123        "End a session: Delete Session Request on GTPv2, Delete PDP Context Request (teardown) on GTPv1, addressed to the peer's control TEID.",
124        vec![session_param()],
125        json!({"type": DELETE, "session": 1}),
126    )
127}
128
129fn gpdu() -> ActionDefinition {
130    action(
131        GPDU,
132        "Send one user packet through a session's tunnel as a G-PDU to the peer's data TEID. Describe the inner IPv4 packet; Rust builds the IP header and the ICMP or UDP header with their checksums. Packets the peer sends back arrive as gtp_tunnel_packet.",
133        vec![
134            session_param(),
135            p("destination", "string", "Inner destination IPv4 address", true),
136            p("source", "string", "Inner source IPv4 address (default: the session's UE address)", false),
137            p("protocol", "string", "Inner packet kind (default icmp_echo)", false)
138                .with_choices(["icmp_echo", "udp"]),
139            p("identifier", "number", "ICMP echo identifier (default 1)", false),
140            p("sequence", "number", "ICMP echo sequence number (default 1)", false),
141            p("source_port", "number", "UDP source port (required for udp)", false),
142            p("destination_port", "number", "UDP destination port (required for udp)", false),
143            p("ttl", "number", "Inner TTL (default 64)", false),
144            p("payload", "string", "Transport payload: text, or hex when encoding is hex", false),
145            p("encoding", "string", "How payload is written (default utf8)", false)
146                .with_choices(["utf8", "hex"]),
147        ],
148        json!({"type": GPDU, "session": 1, "destination": "10.45.0.1", "protocol": "icmp_echo",
149               "identifier": 7, "sequence": 1, "payload": "ping"}),
150    )
151}
152
153fn end_marker() -> ActionDefinition {
154    action(
155        END_MARKER,
156        "Send an End Marker on a session's tunnel: no more packets follow on this path.",
157        vec![session_param()],
158        json!({"type": END_MARKER, "session": 1}),
159    )
160}
161
162fn error_indication() -> ActionDefinition {
163    action(
164        ERROR_INDICATION,
165        "Tell a peer that a TEID it sent a G-PDU to is not a tunnel here (Error Indication). Goes to the address that G-PDU came from; only a TEID seen in a gtp_tunnel_packet with known_tunnel false can be named.",
166        vec![p("teid", "number", "The unknown TEID, from gtp_tunnel_packet", true)],
167        json!({"type": ERROR_INDICATION, "teid": 4096}),
168    )
169}
170
171fn respond() -> ActionDefinition {
172    action(
173        RESPOND,
174        "Answer a request the peer sent (gtp_peer_request), with a cause only. Without an answer the peer retransmits and gives up; nothing is sent on your behalf.",
175        vec![
176            p("sequence", "number", "The request's sequence number, from gtp_peer_request", true),
177            p("cause", "string", "request_accepted, context_not_found, system_failure, … or a number", true),
178        ],
179        json!({"type": RESPOND, "sequence": 1, "cause": "request_accepted"}),
180    )
181}
182
183fn disconnect() -> ActionDefinition {
184    action(
185        "disconnect",
186        "Stop the client. Sessions are not deleted first; delete them if the peer should know.",
187        vec![],
188        json!({"type": "disconnect"}),
189    )
190}
191
192pub fn actions() -> Vec<ActionDefinition> {
193    vec![
194        echo(),
195        create(),
196        modify(),
197        delete(),
198        gpdu(),
199        end_marker(),
200        error_indication(),
201        respond(),
202        disconnect(),
203    ]
204}
205
206fn ev(id: &str, description: &str, params: Vec<Parameter>) -> EventType {
207    EventType::new(id, description, create().example.clone())
208        .with_parameters(params)
209        .with_actions(actions())
210}
211
212pub static READY_EVENT: LazyLock<EventType> = LazyLock::new(|| {
213    ev(
214        "gtp_client_ready",
215        "The client is bound and knows its peer; nothing has been sent yet.",
216        vec![
217            p("peer", "string", "The peer's control-plane address", true),
218            p(
219                "interface",
220                "string",
221                "gn (GTPv1, SGSN to GGSN), s5 (GTPv2, SGW to PGW) or s11 (GTPv2, MME to SGW)",
222                true,
223            ),
224            p(
225                "version",
226                "number",
227                "GTP version on the control plane",
228                true,
229            ),
230            p(
231                "local_control",
232                "string",
233                "This client's control-plane address",
234                true,
235            ),
236            p(
237                "local_user_plane",
238                "string",
239                "This client's GTP-U address",
240                true,
241            ),
242        ],
243    )
244});
245
246pub static RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
247    ev(
248        "gtp_response",
249        "The peer answered a request this client sent.",
250        vec![
251            p(
252                "request",
253                "string",
254                "What it answers: echo_request, create_session, modify_bearer, delete_session",
255                true,
256            ),
257            p("message", "string", "The response's message name", true),
258            p("sequence", "number", "The request's sequence number", true),
259            p(
260                "session",
261                "number",
262                "The session, for session procedures",
263                false,
264            ),
265            p(
266                "cause",
267                "string",
268                "The cause, by name (e.g. request_accepted) or cause_<n>",
269                false,
270            ),
271            p(
272                "accepted",
273                "boolean",
274                "Whether the cause grants the request",
275                false,
276            ),
277            p(
278                "ue_address",
279                "string",
280                "The address the peer gave the UE",
281                false,
282            ),
283            p(
284                "peer_control_teid",
285                "number",
286                "The peer's control TEID",
287                false,
288            ),
289            p(
290                "peer_data_teid",
291                "number",
292                "The peer's user-plane TEID",
293                false,
294            ),
295            p("dns_servers", "array", "DNS servers from the PCO", false),
296            p("recovery", "number", "The peer's restart counter", false),
297        ],
298    )
299});
300
301pub static TIMEOUT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
302    ev(
303        "gtp_timeout",
304        "A request went unanswered through all its retransmissions (T3-RESPONSE x N3-REQUESTS).",
305        vec![
306            p("request", "string", "The request that went unanswered: echo_request, create_session, modify_bearer or delete_session", true),
307            p("sequence", "number", "Its sequence number", true),
308            p("session", "number", "The session, if any", false),
309            p("attempts", "number", "How many times it was sent", true),
310        ],
311    )
312});
313
314pub static TUNNEL_PACKET_EVENT: LazyLock<EventType> = LazyLock::new(|| {
315    ev(
316        "gtp_tunnel_packet",
317        "A G-PDU arrived on the user plane. The inner packet is decoded: addresses, protocol, ports or ICMP type, checksum verdicts, payload.",
318        vec![
319            p("teid", "number", "The TEID it was sent to", true),
320            p("session", "number", "The session that TEID belongs to", false),
321            p("known_tunnel", "boolean", "Whether the TEID is one of this client's", true),
322            p("source_address", "string", "Where it came from", true),
323            p("inner", "object", "The decoded inner packet", true),
324        ],
325    )
326});
327
328pub static TUNNEL_NOTIFICATION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
329    ev(
330        "gtp_tunnel_notification",
331        "The peer sent an Error Indication (a TEID this client used is not a tunnel there) or an End Marker on the user plane.",
332        vec![
333            p("message", "string", "error_indication or end_marker", true),
334            p("teid", "number", "The TEID concerned", true),
335            p("session", "number", "The session it concerns, if known", false),
336            p("peer_address", "string", "The GTP-U peer address the Error Indication names", false),
337        ],
338    )
339});
340
341pub static PEER_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
342    ev(
343        "gtp_peer_request",
344        "The peer sent a session request (e.g. Delete Bearer Request, Delete PDP Context Request). Echo Requests are answered automatically. Answer with gtp_respond.",
345        vec![
346            p("message", "string", "The request's name", true),
347            p("sequence", "number", "Its sequence number, for gtp_respond", true),
348            p("session", "number", "The session its TEID names, if any", false),
349            p("cause", "string", "A cause it carried, if any", false),
350        ],
351    )
352});
353
354/// Bounds and shapes that do not depend on the client's role; the role-specific half
355/// (S11's pgw_address, GTPv1's RAT numbering) is checked again when the action runs.
356pub fn check(v: &Value) -> Result<()> {
357    let kind = v["type"].as_str().unwrap_or_default();
358    match kind {
359        ECHO => {
360            if let Some(plane) = v.get("plane") {
361                ensure!(
362                    matches!(plane.as_str(), Some("control" | "user")),
363                    "plane is control or user"
364                );
365            }
366        }
367        CREATE => {
368            CreateParams::from_action(Role::S5, v)?;
369        }
370        MODIFY | DELETE | END_MARKER => {
371            session(v)?;
372        }
373        GPDU => {
374            session(v)?;
375            gpdu_parts(v)?;
376        }
377        ERROR_INDICATION => {
378            v["teid"]
379                .as_u64()
380                .filter(|t| *t <= u32::MAX as u64)
381                .context("teid is a 32-bit number")?;
382        }
383        RESPOND => {
384            v["sequence"]
385                .as_u64()
386                .filter(|s| *s <= codec::V2_MAX_SEQUENCE as u64)
387                .context("sequence is the request's sequence number")?;
388            msg::cause_value(GtpVersion::V2, v.get("cause").context("cause is required")?)?;
389        }
390        other => bail!("Unknown GTP client action {other:?}"),
391    }
392    Ok(())
393}
394
395pub fn session(v: &Value) -> Result<u32> {
396    v["session"]
397        .as_u64()
398        .filter(|s| *s <= u32::MAX as u64)
399        .map(|s| s as u32)
400        .context("session is required: the number gtp_response gave")
401}
402
403/// The validated pieces of a gtp_send_gpdu.
404pub struct GpduParts {
405    pub destination: std::net::Ipv4Addr,
406    pub source: Option<std::net::Ipv4Addr>,
407    pub inner: msg::Inner,
408    pub ttl: u8,
409    pub payload: Vec<u8>,
410}
411
412fn v4(v: &Value, key: &str) -> Result<Option<std::net::Ipv4Addr>> {
413    match v.get(key).and_then(Value::as_str) {
414        None => Ok(None),
415        Some(s) => s.parse::<std::net::Ipv4Addr>().map(Some).with_context(|| {
416            format!("{key} {s:?} is not an IPv4 address (the inner packet is IPv4)")
417        }),
418    }
419}
420
421fn port(v: &Value, key: &str) -> Result<u16> {
422    v.get(key)
423        .and_then(Value::as_u64)
424        .filter(|p| *p <= 65535)
425        .map(|p| p as u16)
426        .with_context(|| format!("{key} is a port number, required for udp"))
427}
428
429pub fn gpdu_parts(v: &Value) -> Result<GpduParts> {
430    let destination = v4(v, "destination")?.context("destination is required")?;
431    let source = v4(v, "source")?;
432    let u16_or = |k: &str, d: u16| -> Result<u16> {
433        match v.get(k) {
434            None => Ok(d),
435            Some(x) => x
436                .as_u64()
437                .filter(|n| *n <= 65535)
438                .map(|n| n as u16)
439                .with_context(|| format!("{k} is 0..=65535")),
440        }
441    };
442    let inner = match v
443        .get("protocol")
444        .and_then(Value::as_str)
445        .unwrap_or("icmp_echo")
446    {
447        "icmp_echo" => msg::Inner::IcmpEcho {
448            identifier: u16_or("identifier", 1)?,
449            sequence: u16_or("sequence", 1)?,
450        },
451        "udp" => msg::Inner::Udp {
452            source_port: port(v, "source_port")?,
453            destination_port: port(v, "destination_port")?,
454        },
455        other => bail!("protocol {other:?} is icmp_echo or udp"),
456    };
457    let ttl = match v.get("ttl") {
458        None => 64,
459        Some(x) => x
460            .as_u64()
461            .filter(|n| (1..=255).contains(n))
462            .context("ttl is 1..=255")? as u8,
463    };
464    let text = v.get("payload").and_then(Value::as_str).unwrap_or("");
465    let payload = match v.get("encoding").and_then(Value::as_str).unwrap_or("utf8") {
466        "utf8" => text.as_bytes().to_vec(),
467        "hex" => hex::decode(text.trim()).context("payload is not valid hex")?,
468        other => bail!("encoding {other:?} is utf8 or hex"),
469    };
470    ensure!(
471        payload.len() <= msg::MAX_INNER_PAYLOAD,
472        "payload of {} octets is past {}",
473        payload.len(),
474        msg::MAX_INNER_PAYLOAD
475    );
476    Ok(GpduParts {
477        destination,
478        source,
479        inner,
480        ttl,
481        payload,
482    })
483}
484
485impl Protocol for GtpClientProtocol {
486    fn protocol_name(&self) -> &'static str {
487        "GTP"
488    }
489    fn stack_name(&self) -> &'static str {
490        "ETH>IP>UDP>GTP"
491    }
492    fn keywords(&self) -> Vec<&'static str> {
493        vec![
494            "gtp client",
495            "gtp-c client",
496            "sgsn",
497            "mme",
498            "sgw-c",
499            "create session request",
500            "create pdp context",
501        ]
502    }
503    fn description(&self) -> &'static str {
504        "GTP client: the near end of a mobile-core path (an SGSN on Gn, an SGW on S5/S8 or an MME on S11) that creates, modifies and deletes sessions and carries user packets through the tunnel"
505    }
506    fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
507        actions()
508    }
509    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
510        vec![]
511    }
512    fn get_event_types(&self) -> Vec<EventType> {
513        vec![
514            READY_EVENT.clone(),
515            RESPONSE_EVENT.clone(),
516            TIMEOUT_EVENT.clone(),
517            TUNNEL_PACKET_EVENT.clone(),
518            TUNNEL_NOTIFICATION_EVENT.clone(),
519            PEER_REQUEST_EVENT.clone(),
520        ]
521    }
522    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
523        vec![
524            ParameterDefinition {
525                name: "interface".into(),
526                type_hint: "string".into(),
527                description: "Which node this client is: s5 (GTPv2, an SGW towards a PGW), s11 (GTPv2, an MME towards an SGW) or gn (GTPv1, an SGSN towards a GGSN)".into(),
528                required: false,
529                example: json!("s5"),
530                default: Some(json!(super::DEFAULT_INTERFACE)),
531            },
532            ParameterDefinition {
533                name: "local_port".into(),
534                type_hint: "number".into(),
535                description: "UDP port the control plane sends from (default ephemeral; 2123 to receive requests a peer starts)".into(),
536                required: false,
537                example: json!(2123),
538                default: None,
539            },
540            ParameterDefinition {
541                name: "user_plane_port".into(),
542                type_hint: "number".into(),
543                description: "UDP port of this client's GTP-U endpoint. Peers send G-PDUs to 2152 whatever the F-TEID says, so that is the default; when 2152 is taken the client says so and uses an ephemeral port. 0 asks for an ephemeral port".into(),
544                required: false,
545                example: json!(2152),
546                default: Some(json!(codec::GTPU_PORT)),
547            },
548            ParameterDefinition {
549                name: "peer_user_plane_port".into(),
550                type_hint: "number".into(),
551                description: "UDP port G-PDUs and user-plane echoes are sent to on the peer".into(),
552                required: false,
553                example: json!(2152),
554                default: Some(json!(codec::GTPU_PORT)),
555            },
556            ParameterDefinition {
557                name: "t3_response_ms".into(),
558                type_hint: "number".into(),
559                description: "T3-RESPONSE: milliseconds to wait for an answer before retransmitting (100..=60000)".into(),
560                required: false,
561                example: json!(3000),
562                default: Some(json!(super::T3_RESPONSE.as_millis() as u64)),
563            },
564            ParameterDefinition {
565                name: "n3_requests".into(),
566                type_hint: "number".into(),
567                description: "N3-REQUESTS: retransmissions after the first send before gtp_timeout (0..=10)".into(),
568                required: false,
569                example: json!(3),
570                default: Some(json!(super::N3_REQUESTS)),
571            },
572        ]
573    }
574    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
575        use crate::protocol::metadata::*;
576        ProtocolMetadataV2::builder()
577            .state(DevelopmentState::Experimental)
578            .privilege_requirement(PrivilegeRequirement::None)
579            .implementation("The server's GTP codec (src/server/gtp/codec.rs) in the requesting role: Create Session / Create PDP Context, Modify Bearer / Update PDP Context, Delete Session / Delete PDP Context and Echo, with sequence numbers, TEID allocation, F-TEIDs and GSN addresses in Rust; responses matched by sequence and message type; T3-RESPONSE/N3-REQUESTS retransmission; a GTP-U endpoint that sends G-PDUs whose inner IPv4/ICMP/UDP packet Rust builds with its checksums, answers echoes, and decodes what arrives")
580            .llm_control("Which procedures to run and when, the subscriber (IMSI, MSISDN, MEI), APN, PDN type, bearer QoS and AMBR, what to send through the tunnel, and how to answer requests the peer starts")
581            .e2e_testing("tests/client/gtp: wmnsk/go-gtp v0.8.12 as a PGW (GTPv2 S5/S8, GTP-U) decoding every request NetGet sends, and Osmocom osmo-ggsn as a GGSN (GTPv1 Gn) whose kernel tun device answers an ICMP echo NetGet tunnels to it; tshark's gtp/gtpv2 dissectors read both directions")
582            .notes("Experimental: the client Beta bar's peers exist and the tests fail rather than skip without them, but osmo-ggsn runs only on Linux with a tun device, so its half of the evidence runs in the gtp-client job of protocol-pairs.yml (dispatched by hand) and not on every change. One peer per client. The inner packet is IPv4 only. No secondary PDP contexts or dedicated bearers, no handover, no GTP' or GTPv0.")
583            .max_inbound_bytes(msg::MAX_DATAGRAM)
584            .build()
585    }
586    fn example_prompt(&self) -> &'static str {
587        "Act as an SGW towards the PGW at 192.0.2.10: create a session for IMSI 001010123456789 on APN internet, then ping 8.8.8.8 through the tunnel"
588    }
589    fn get_startup_examples(&self) -> StartupExamples {
590        let llm = json!({"type":"open_client","protocol":"gtp","remote_addr":"127.0.0.1:2123",
591            "startup_params": {"interface": "s5"},
592            "instruction":"Create a session for IMSI 001010123456789 on APN internet, then send one ICMP echo through the tunnel to 10.45.0.1"});
593        let mut static_example = llm.clone();
594        static_example["event_handlers"] = json!([
595            {"event_pattern":"gtp_client_ready","handler":{"type":"static","actions":[create().example]}},
596            {"event_pattern":"*","handler":{"type":"static","actions":[]}}
597        ]);
598        let mut scripted = static_example.clone();
599        scripted["event_handlers"][1] = json!({"event_pattern":"gtp_response","handler":{"type":"script","language":"python",
600            "code":"import json,sys\ne=json.load(sys.stdin)['event']\na=[]\nif e.get('request')=='create_session' and e.get('accepted'):\n  a=[{'type':'gtp_send_gpdu','session':e['session'],'destination':'10.45.0.1','payload':'ping'}]\nprint(json.dumps({'actions':a}))"}});
601        StartupExamples::new(llm, scripted, static_example)
602    }
603    fn group_name(&self) -> &'static str {
604        "Mobile"
605    }
606}
607
608impl Client for GtpClientProtocol {
609    fn connect(
610        &self,
611        ctx: ConnectContext,
612    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
613    {
614        Box::pin(super::connect(ctx))
615    }
616    fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
617        if v["type"] == "disconnect" {
618            return Ok(ClientActionResult::Disconnect);
619        }
620        check(&v)?;
621        Ok(ClientActionResult::Custom {
622            name: v["type"].as_str().unwrap_or_default().to_string(),
623            data: v,
624        })
625    }
626}