1use 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
354pub 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
403pub 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}