1pub mod actions;
25pub mod codec;
26
27pub use actions::GtpProtocol;
28
29use anyhow::{Context, Result};
30use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
31use std::sync::Arc;
32use tokio::net::UdpSocket;
33use tokio::sync::mpsc;
34use tracing::{debug, warn};
35
36use crate::llm::action_helper::call_llm;
37use crate::llm::ollama_client::OllamaClient;
38use crate::llm::ActionResult;
39use crate::logging::emit::Log;
40use crate::protocol::{Event, SpawnContext};
41use crate::server::connection::ConnectionId;
42use crate::state::app_state::AppState;
43use crate::state::ServerId;
44use crate::utils::wire_failure::WireFailure;
45
46use actions::{
47 GTP_CREATE_SESSION_REQUEST_EVENT, GTP_DELETE_SESSION_REQUEST_EVENT, GTP_ECHO_REQUEST_EVENT,
48 GTP_GPDU_RECEIVED_EVENT, GTP_UPDATE_CONTEXT_REQUEST_EVENT,
49};
50use codec::{GtpV1Header, GtpV1Ie, GtpV1Message, GtpV2Header, GtpV2Ie, GtpV2Message, GtpVersion};
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum Plane {
55 Control,
56 User,
57}
58
59impl Plane {
60 fn as_str(self) -> &'static str {
61 match self {
62 Plane::Control => "control",
63 Plane::User => "user",
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum Decision {
74 ModelEcho,
75 ModelAccept,
77 ModelReject,
79 ModelGpdu,
80 ModelErrorIndication,
81 ModelSilent,
83 FailClosedNoAction,
85 FailClosedActionError,
87 FailClosedLlmError,
89}
90
91impl Decision {
92 pub fn as_str(self) -> &'static str {
95 match self {
96 Decision::ModelEcho => "model_echo",
97 Decision::ModelAccept => "model_accept",
98 Decision::ModelReject => "model_reject",
99 Decision::ModelGpdu => "model_gpdu",
100 Decision::ModelErrorIndication => "model_error_indication",
101 Decision::ModelSilent => "model_silent",
102 Decision::FailClosedNoAction => "fail_closed_no_action",
103 Decision::FailClosedActionError => "fail_closed_action_error",
104 Decision::FailClosedLlmError => "fail_closed_llm_error",
105 }
106 }
107
108 pub fn is_fail_closed(self) -> bool {
109 matches!(
110 self,
111 Decision::FailClosedNoAction
112 | Decision::FailClosedActionError
113 | Decision::FailClosedLlmError
114 )
115 }
116}
117
118#[derive(Clone)]
121struct Shared {
122 llm_client: OllamaClient,
123 state: Arc<AppState>,
124 status_tx: mpsc::UnboundedSender<String>,
125 server_id: ServerId,
126 control_socket: Arc<UdpSocket>,
127 user_socket: Option<Arc<UdpSocket>>,
128 control_addr: SocketAddr,
129 user_addr: Option<SocketAddr>,
130}
131
132impl Shared {
133 fn socket_for(&self, plane: Plane) -> &Arc<UdpSocket> {
134 match plane {
135 Plane::User => self.user_socket.as_ref().unwrap_or(&self.control_socket),
136 Plane::Control => &self.control_socket,
137 }
138 }
139
140 fn local_addr(&self, plane: Plane) -> SocketAddr {
141 match plane {
142 Plane::User => self.user_addr.unwrap_or(self.control_addr),
143 Plane::Control => self.control_addr,
144 }
145 }
146
147 fn advertised_control_ip(&self) -> IpAddr {
149 concrete_ip(self.control_addr.ip())
150 }
151
152 fn advertised_user_ip(&self) -> IpAddr {
154 concrete_ip(self.user_addr.unwrap_or(self.control_addr).ip())
155 }
156}
157
158fn concrete_ip(ip: IpAddr) -> IpAddr {
162 match ip {
163 IpAddr::V4(v4) if v4.is_unspecified() => IpAddr::V4(Ipv4Addr::LOCALHOST),
164 IpAddr::V6(v6) if v6.is_unspecified() => IpAddr::V6(Ipv6Addr::LOCALHOST),
165 other => other,
166 }
167}
168
169struct RequestContext {
171 version: GtpVersion,
172 message_type: u8,
173 message_name: &'static str,
174 sequence: u32,
175 default_response_teid: u32,
177 bearer_id: Option<u8>,
179 plane: Plane,
180 peer_addr: SocketAddr,
181}
182
183pub struct GtpServer;
184
185impl GtpServer {
186 pub async fn spawn_with_llm_actions(ctx: SpawnContext) -> Result<SocketAddr> {
192 let listen_addr = ctx.legacy_listen_addr();
193 let SpawnContext {
194 llm_client,
195 state,
196 status_tx,
197 server_id,
198 startup_params,
199 ..
200 } = ctx;
201
202 let (requested_user_port, enable_user_plane) = match &startup_params {
205 Some(params) => {
206 let port = match params.get_optional_u64("user_plane_port")? {
207 Some(v) if v <= u16::MAX as u64 => Some(v as u16),
208 Some(v) => anyhow::bail!(
209 "GTP user_plane_port must be a UDP port number 0-65535, got {v}"
210 ),
211 None => None,
212 };
213 (
214 port,
215 params
216 .get_optional_bool("enable_user_plane")?
217 .unwrap_or(true),
218 )
219 }
220 None => (None, true),
221 };
222
223 let control_socket =
224 Arc::new(UdpSocket::bind(listen_addr).await.with_context(|| {
225 format!("GTP failed to bind the control plane on {listen_addr}")
226 })?);
227 let control_addr = control_socket.local_addr()?;
228
229 let log = Log::new(Some(&status_tx));
230 log.info(format!("GTP-C server listening on {control_addr}"));
231
232 let (user_socket, user_addr) = if enable_user_plane {
233 let port = requested_user_port.unwrap_or({
237 if control_addr.port() == codec::GTPC_PORT {
238 codec::GTPU_PORT
239 } else {
240 0
241 }
242 });
243 let addr = SocketAddr::new(control_addr.ip(), port);
244 let socket = Arc::new(UdpSocket::bind(addr).await.with_context(|| {
245 format!(
246 "GTP failed to bind the user plane on {addr}. Pass a different \
247 user_plane_port, or enable_user_plane: false for a control-plane-only \
248 node."
249 )
250 })?);
251 let local = socket.local_addr()?;
252 log.info(format!("GTP-U user plane bound to {local}"));
256 (Some(socket), Some(local))
257 } else {
258 log.info("GTP-U disabled by enable_user_plane: false; control plane only");
259 (None, None)
260 };
261
262 let shared = Shared {
263 llm_client,
264 state: state.clone(),
265 status_tx: status_tx.clone(),
266 server_id,
267 control_socket: control_socket.clone(),
268 user_socket: user_socket.clone(),
269 control_addr,
270 user_addr,
271 };
272
273 let control_loop = tokio::spawn(Self::receive_loop(
274 shared.clone(),
275 control_socket,
276 Plane::Control,
277 ));
278 state.register_server_task(server_id, control_loop).await;
279
280 if let Some(socket) = user_socket {
281 let user_loop = tokio::spawn(Self::receive_loop(shared, socket, Plane::User));
282 state.register_server_task(server_id, user_loop).await;
283 }
284
285 Ok(control_addr)
286 }
287
288 async fn receive_loop(shared: Shared, socket: Arc<UdpSocket>, plane: Plane) {
289 let local_addr = shared.local_addr(plane);
290 Log::new(Some(&shared.status_tx)).info(format!(
291 "GTP {} plane receive loop started on {local_addr}",
292 plane.as_str()
293 ));
294
295 let mut buffer = vec![0u8; 2048];
298
299 loop {
300 let (n, peer_addr) = match socket.recv_from(&mut buffer).await {
301 Ok(pair) => pair,
302 Err(e) => {
303 Log::new(Some(&shared.status_tx))
304 .error(format!("GTP {} plane receive error: {e}", plane.as_str()));
305 break;
306 }
307 };
308
309 let data = buffer[..n].to_vec();
310 let shared = shared.clone();
311 let task_owner = shared.state.clone();
313 task_owner
314 .spawn_server_task(shared.server_id, async move {
315 Self::handle_datagram(shared, data, peer_addr, plane).await;
316 })
317 .await;
318 }
319 }
320
321 async fn handle_datagram(shared: Shared, data: Vec<u8>, peer_addr: SocketAddr, plane: Plane) {
322 let connection_id = Self::record_connection(&shared, peer_addr, plane, data.len()).await;
323
324 let log = Log::new(Some(&shared.status_tx));
325 log.debug(format!(
326 "GTP received {} octets from {peer_addr} on the {} plane",
327 data.len(),
328 plane.as_str()
329 ));
330 log.trace(format!("GTP received (hex): {}", hex::encode(&data)));
331
332 match codec::peek_version(&data) {
333 Some(1) => Self::handle_v1(shared, data, peer_addr, plane, connection_id).await,
334 Some(2) => Self::handle_v2(shared, data, peer_addr, plane, connection_id).await,
335 Some(other) => {
336 warn!("GTP version {other} from {peer_addr} is not supported");
340 if plane == Plane::Control {
341 let reply = GtpV1Message {
342 header: GtpV1Header::with_sequence(codec::V1_VERSION_NOT_SUPPORTED, 0, 0),
343 body: Vec::new(),
344 }
345 .encode();
346 Self::send(&shared, &reply, peer_addr, plane, connection_id).await;
347 }
348 }
349 None => debug!("GTP empty datagram from {peer_addr} ignored"),
350 }
351 }
352
353 async fn handle_v1(
358 shared: Shared,
359 data: Vec<u8>,
360 peer_addr: SocketAddr,
361 plane: Plane,
362 connection_id: ConnectionId,
363 ) {
364 let message = match GtpV1Message::decode(&data) {
365 Ok(m) => m,
366 Err(e) => {
367 Log::new(Some(&shared.status_tx))
368 .warn(format!("GTPv1 dropped datagram from {peer_addr}: {e}"));
369 return;
370 }
371 };
372
373 let message_type = message.header.message_type;
374 let message_name = codec::v1_message_name(message_type);
375 let sequence = message.header.sequence.unwrap_or(0) as u32;
376
377 if message_type == codec::V1_G_PDU {
379 let ctx = RequestContext {
380 version: GtpVersion::V1,
381 message_type,
382 message_name,
383 sequence,
384 default_response_teid: message.header.teid,
385 bearer_id: None,
386 plane,
387 peer_addr,
388 };
389 let data = Self::gpdu_event_data(&message, peer_addr);
390 Self::consult(shared, ctx, >P_GPDU_RECEIVED_EVENT, data, connection_id).await;
391 return;
392 }
393
394 let ies = match codec::parse_v1_ies(&message.body) {
395 Ok(ies) => ies,
396 Err(e) => {
397 Log::new(Some(&shared.status_tx)).warn(format!(
398 "GTPv1 {message_name} from {peer_addr} has unparseable information \
399 elements: {e}"
400 ));
401 return;
402 }
403 };
404
405 let peer_control_teid = codec::find_v1(&ies, codec::V1_IE_TEID_CONTROL_PLANE)
409 .and_then(|v| v.try_into().ok())
410 .map(u32::from_be_bytes);
411 let peer_data_teid = codec::find_v1(&ies, codec::V1_IE_TEID_DATA_I)
412 .and_then(|v| v.try_into().ok())
413 .map(u32::from_be_bytes);
414 let bearer_id = codec::find_v1(&ies, codec::V1_IE_NSAPI)
415 .and_then(|v| v.first().copied())
416 .map(|v| v & 0x0F);
417
418 let ctx = RequestContext {
419 version: GtpVersion::V1,
420 message_type,
421 message_name,
422 sequence,
423 default_response_teid: peer_control_teid.unwrap_or(message.header.teid),
424 bearer_id,
425 plane,
426 peer_addr,
427 };
428
429 let mut base = serde_json::json!({
430 "version": 1,
431 "message": message_name,
432 "sequence": sequence,
433 "teid": message.header.teid,
434 "source_address": peer_addr.to_string(),
435 "information_elements": Self::v1_ie_summary(&ies),
436 });
437
438 match message_type {
439 codec::V1_ECHO_REQUEST => {
440 base["plane"] = serde_json::json!(plane.as_str());
441 Self::consult(shared, ctx, >P_ECHO_REQUEST_EVENT, base, connection_id).await;
442 }
443 codec::V1_CREATE_PDP_CONTEXT_REQUEST => {
444 Self::add_v1_subscriber_fields(&mut base, &ies);
445 base["control_teid"] = serde_json::json!(peer_control_teid);
446 base["data_teid"] = serde_json::json!(peer_data_teid);
447 base["nsapi"] = serde_json::json!(bearer_id);
448 Self::consult(
449 shared,
450 ctx,
451 >P_CREATE_SESSION_REQUEST_EVENT,
452 base,
453 connection_id,
454 )
455 .await;
456 }
457 codec::V1_UPDATE_PDP_CONTEXT_REQUEST => {
458 Self::add_v1_subscriber_fields(&mut base, &ies);
459 base["control_teid"] = serde_json::json!(peer_control_teid);
460 base["data_teid"] = serde_json::json!(peer_data_teid);
461 base["nsapi"] = serde_json::json!(bearer_id);
462 Self::consult(
463 shared,
464 ctx,
465 >P_UPDATE_CONTEXT_REQUEST_EVENT,
466 base,
467 connection_id,
468 )
469 .await;
470 }
471 codec::V1_DELETE_PDP_CONTEXT_REQUEST => {
472 base["nsapi"] = serde_json::json!(bearer_id);
473 base["teardown_indicator"] =
474 serde_json::json!(codec::find_v1(&ies, codec::V1_IE_TEARDOWN_IND)
475 .and_then(|v| v.first().copied())
476 .map(|v| v & 0x01 == 1));
477 Self::consult(
478 shared,
479 ctx,
480 >P_DELETE_SESSION_REQUEST_EVENT,
481 base,
482 connection_id,
483 )
484 .await;
485 }
486 other => {
487 debug!(
490 "GTPv1 {} (type {other}) from {peer_addr} noted and not answered",
491 message_name
492 );
493 }
494 }
495 }
496
497 fn add_v1_subscriber_fields(base: &mut serde_json::Value, ies: &[GtpV1Ie]) {
498 if let Some(v) = codec::find_v1(ies, codec::V1_IE_IMSI) {
501 base["imsi"] = serde_json::json!(codec::decode_tbcd(v));
502 }
503 if let Some(v) = codec::find_v1(ies, codec::V1_IE_MSISDN) {
504 let digits = if v.len() > 1 {
506 codec::decode_tbcd(&v[1..])
507 } else {
508 String::new()
509 };
510 base["msisdn"] = serde_json::json!(digits);
511 }
512 if let Some(v) = codec::find_v1(ies, codec::V1_IE_ACCESS_POINT_NAME) {
513 if let Some(apn) = codec::decode_apn(v) {
514 base["apn"] = serde_json::json!(apn);
515 }
516 }
517 if let Some(v) = codec::find_v1(ies, codec::V1_IE_END_USER_ADDRESS) {
518 let (pdp_type, addr) = codec::decode_end_user_address(v);
519 base["pdp_type"] = serde_json::json!(pdp_type);
520 base["requested_address"] = serde_json::json!(addr.map(|a| a.to_string()));
521 }
522 if let Some(v) = codec::find_v1(ies, codec::V1_IE_RAT_TYPE) {
523 if let Some(rat) = v.first() {
524 base["rat_type"] = serde_json::json!(codec::rat_type_name(GtpVersion::V1, *rat));
525 }
526 }
527 if let Some(v) = codec::find_v1(ies, codec::V1_IE_GSN_ADDRESS) {
529 if let Some(addr) = gsn_address(v) {
530 base["peer_address"] = serde_json::json!(addr.to_string());
531 }
532 }
533 }
534
535 fn v1_ie_summary(ies: &[GtpV1Ie]) -> serde_json::Value {
536 serde_json::Value::Array(
537 ies.iter()
538 .map(|ie| {
539 serde_json::json!({
540 "type": ie.ie_type,
541 "name": codec::v1_ie_name(ie.ie_type),
542 "length": ie.value.len(),
543 })
544 })
545 .collect(),
546 )
547 }
548
549 fn gpdu_event_data(message: &GtpV1Message, peer_addr: SocketAddr) -> serde_json::Value {
550 let mut data = serde_json::json!({
551 "teid": message.header.teid,
552 "sequence": message.header.sequence,
553 "source_address": peer_addr.to_string(),
554 });
555
556 if let Some(inner) = codec::decode_inner_ip(&message.body) {
557 data["inner_ip"] = serde_json::json!({
558 "version": inner.version,
559 "source": inner.source.to_string(),
560 "destination": inner.destination.to_string(),
561 "protocol": inner.protocol,
562 "protocol_name": inner.protocol_name,
563 "ttl": inner.ttl,
564 "length": inner.length,
565 "source_port": inner.source_port,
566 "destination_port": inner.destination_port,
567 });
568 if let Some(offset) = inner.payload_offset {
569 if offset < message.body.len() {
570 let payload = &message.body[offset..];
571 let printable = payload
572 .iter()
573 .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace());
574 if printable {
575 data["payload"] =
576 serde_json::json!(String::from_utf8_lossy(payload).to_string());
577 data["payload_encoding"] = serde_json::json!("utf8");
578 } else {
579 data["payload"] = serde_json::json!(hex::encode(payload));
580 data["payload_encoding"] = serde_json::json!("hex");
581 }
582 }
583 }
584 }
585
586 data
587 }
588
589 async fn handle_v2(
594 shared: Shared,
595 data: Vec<u8>,
596 peer_addr: SocketAddr,
597 plane: Plane,
598 connection_id: ConnectionId,
599 ) {
600 if plane == Plane::User {
601 warn!("GTPv2 datagram arrived on the user plane from {peer_addr}; dropped");
603 return;
604 }
605
606 let message = match GtpV2Message::decode(&data) {
607 Ok(m) => m,
608 Err(e) => {
609 Log::new(Some(&shared.status_tx))
610 .warn(format!("GTPv2 dropped datagram from {peer_addr}: {e}"));
611 return;
612 }
613 };
614
615 let message_type = message.header.message_type;
616 let message_name = codec::v2_message_name(message_type);
617 let header_teid = message.header.teid.unwrap_or(0);
618
619 let sender_fteid = message
620 .find_instance(codec::V2_IE_FTEID, 0)
621 .and_then(codec::decode_fteid);
622 let bearer_id = message
623 .find(codec::V2_IE_BEARER_CONTEXT)
624 .and_then(|v| codec::parse_v2_grouped(v).ok())
625 .and_then(|inner| {
626 inner
627 .iter()
628 .find(|ie| ie.ie_type == codec::V2_IE_EBI)
629 .and_then(|ie| ie.value.first().copied())
630 })
631 .or_else(|| {
632 message
633 .find(codec::V2_IE_EBI)
634 .and_then(|v| v.first().copied())
635 })
636 .map(|v| v & 0x0F);
637
638 let ctx = RequestContext {
639 version: GtpVersion::V2,
640 message_type,
641 message_name,
642 sequence: message.header.sequence,
643 default_response_teid: sender_fteid.map(|f| f.1).unwrap_or(header_teid),
644 bearer_id,
645 plane,
646 peer_addr,
647 };
648
649 let mut base = serde_json::json!({
650 "version": 2,
651 "message": message_name,
652 "sequence": message.header.sequence,
653 "teid": header_teid,
654 "source_address": peer_addr.to_string(),
655 "information_elements": serde_json::Value::Array(
656 message.ies.iter().map(|ie| serde_json::json!({
657 "type": ie.ie_type,
658 "name": codec::v2_ie_name(ie.ie_type),
659 "instance": ie.instance,
660 "length": ie.value.len(),
661 })).collect(),
662 ),
663 });
664
665 match message_type {
666 codec::V2_ECHO_REQUEST => {
667 base["plane"] = serde_json::json!(plane.as_str());
668 Self::consult(shared, ctx, >P_ECHO_REQUEST_EVENT, base, connection_id).await;
669 }
670 codec::V2_CREATE_SESSION_REQUEST => {
671 Self::add_v2_subscriber_fields(&mut base, &message, sender_fteid);
672 base["nsapi"] = serde_json::json!(bearer_id);
673 Self::consult(
674 shared,
675 ctx,
676 >P_CREATE_SESSION_REQUEST_EVENT,
677 base,
678 connection_id,
679 )
680 .await;
681 }
682 codec::V2_MODIFY_BEARER_REQUEST => {
683 Self::add_v2_subscriber_fields(&mut base, &message, sender_fteid);
684 base["nsapi"] = serde_json::json!(bearer_id);
685 Self::consult(
686 shared,
687 ctx,
688 >P_UPDATE_CONTEXT_REQUEST_EVENT,
689 base,
690 connection_id,
691 )
692 .await;
693 }
694 codec::V2_DELETE_SESSION_REQUEST => {
695 base["nsapi"] = serde_json::json!(bearer_id);
696 Self::consult(
697 shared,
698 ctx,
699 >P_DELETE_SESSION_REQUEST_EVENT,
700 base,
701 connection_id,
702 )
703 .await;
704 }
705 other => debug!(
706 "GTPv2 {} (type {other}) from {peer_addr} noted and not answered",
707 message_name
708 ),
709 }
710 }
711
712 fn add_v2_subscriber_fields(
713 base: &mut serde_json::Value,
714 message: &GtpV2Message,
715 sender_fteid: Option<(u8, u32, Option<IpAddr>)>,
716 ) {
717 if let Some(v) = message.find(codec::V2_IE_IMSI) {
718 base["imsi"] = serde_json::json!(codec::decode_tbcd(v));
719 }
720 if let Some(v) = message.find(codec::V2_IE_MSISDN) {
721 base["msisdn"] = serde_json::json!(codec::decode_tbcd(v));
722 }
723 if let Some(v) = message.find(codec::V2_IE_APN) {
724 if let Some(apn) = codec::decode_apn(v) {
725 base["apn"] = serde_json::json!(apn);
726 }
727 }
728 if let Some(v) = message.find(codec::V2_IE_PAA) {
729 let (pdn_type, addr) = codec::decode_paa(v);
730 base["pdp_type"] = serde_json::json!(pdn_type);
731 base["requested_address"] = serde_json::json!(addr.map(|a| a.to_string()));
732 }
733 if let Some(v) = message.find(codec::V2_IE_RAT_TYPE) {
734 if let Some(rat) = v.first() {
735 base["rat_type"] = serde_json::json!(codec::rat_type_name(GtpVersion::V2, *rat));
736 }
737 }
738 if let Some((_, teid, addr)) = sender_fteid {
739 base["control_teid"] = serde_json::json!(teid);
740 base["peer_address"] = serde_json::json!(addr.map(|a| a.to_string()));
741 }
742 if let Some(inner) = message
744 .find(codec::V2_IE_BEARER_CONTEXT)
745 .and_then(|v| codec::parse_v2_grouped(v).ok())
746 {
747 if let Some(fteid) = inner
748 .iter()
749 .find(|ie| ie.ie_type == codec::V2_IE_FTEID)
750 .and_then(|ie| codec::decode_fteid(&ie.value))
751 {
752 base["data_teid"] = serde_json::json!(fteid.1);
753 }
754 }
755 }
756
757 async fn consult(
762 shared: Shared,
763 ctx: RequestContext,
764 event_type: &'static crate::protocol::EventType,
765 event_data: serde_json::Value,
766 connection_id: ConnectionId,
767 ) {
768 let protocol = GtpProtocol::new();
769 let event = Event::new(event_type, event_data);
770
771 let outcome = call_llm(
772 &shared.llm_client,
773 &shared.state,
774 shared.server_id,
775 Some(connection_id),
776 &event,
777 &protocol,
778 )
779 .await;
780
781 let (decision, reply) = Self::decide(&shared, &ctx, outcome);
782
783 let summary = format!(
784 "GTP v{} {} seq={} from {} decision={}",
785 ctx.version.as_number(),
786 ctx.message_name,
787 ctx.sequence,
788 ctx.peer_addr,
789 decision.as_str()
790 );
791 let log = Log::new(Some(&shared.status_tx));
792 if decision.is_fail_closed() {
793 log.error(format!(
794 "{summary} (refused because no usable decision was produced)"
795 ));
796 } else {
797 log.info(&summary);
798 }
799
800 if let Some((bytes, plane)) = reply {
801 Self::send(&shared, &bytes, ctx.peer_addr, plane, connection_id).await;
802 }
803 }
804
805 fn decide(
817 shared: &Shared,
818 ctx: &RequestContext,
819 outcome: Result<crate::llm::ExecutionResult>,
820 ) -> (Decision, Option<(Vec<u8>, Plane)>) {
821 let log = Log::new(Some(&shared.status_tx));
822
823 let execution = match outcome {
824 Ok(result) => {
825 for message in &result.messages {
826 log.info(message);
827 }
828 result
829 }
830 Err(e) => {
831 let category = WireFailure::classify(&e);
834 log.error(format!(
835 "GTP LLM call failed for {} from {} (category={}): {e}",
836 ctx.message_name,
837 ctx.peer_addr,
838 if category.is_overloaded() {
839 "overloaded"
840 } else {
841 "unavailable"
842 }
843 ));
844 return (
845 Decision::FailClosedLlmError,
846 Self::synthesised_refusal(ctx, category),
847 );
848 }
849 };
850
851 let mut first_error: Option<String> = None;
852 for result in &execution.protocol_results {
853 let ActionResult::Custom { name, data } = result else {
854 continue;
855 };
856 match Self::render(shared, ctx, name, data) {
857 Ok(Some(rendered)) => return rendered,
858 Ok(None) => {}
859 Err(e) => {
860 log.error(format!(
861 "GTP could not encode the model's {name} for {} from {}: {e}",
862 ctx.message_name, ctx.peer_addr
863 ));
864 first_error.get_or_insert(e.to_string());
865 }
866 }
867 }
868
869 if first_error.is_some() {
870 return (
871 Decision::FailClosedActionError,
872 Self::synthesised_refusal(ctx, WireFailure::Unavailable),
873 );
874 }
875 (
876 Decision::FailClosedNoAction,
877 Self::synthesised_refusal(ctx, WireFailure::Unavailable),
878 )
879 }
880
881 fn checked_sequence(
886 data: &serde_json::Value,
887 version: GtpVersion,
888 fallback: u32,
889 ) -> Result<u32> {
890 match data.get("sequence").and_then(|v| v.as_u64()) {
891 Some(v) if v > version.max_sequence() as u64 => anyhow::bail!(
892 "sequence {v} does not fit a GTPv{} header: the field is {} bits, so the \
893 largest value is {}. Copy the request's own sequence from the event, or \
894 omit 'sequence' entirely and the server echoes it for you.",
895 version.as_number(),
896 version.sequence_bits(),
897 version.max_sequence()
898 ),
899 Some(v) => Ok(v as u32),
900 None => Ok(fallback),
901 }
902 }
903
904 #[allow(clippy::type_complexity)]
909 fn render(
910 shared: &Shared,
911 ctx: &RequestContext,
912 name: &str,
913 data: &serde_json::Value,
914 ) -> Result<Option<(Decision, Option<(Vec<u8>, Plane)>)>> {
915 let sequence = Self::checked_sequence(data, ctx.version, ctx.sequence)?;
923 let teid = data
924 .get("teid")
925 .and_then(|v| v.as_u64())
926 .map(|v| v as u32)
927 .unwrap_or(ctx.default_response_teid);
928
929 match name {
930 actions::RESULT_NO_RESPONSE => Ok(Some((Decision::ModelSilent, None))),
931
932 actions::RESULT_ECHO_RESPONSE => {
933 let recovery = data.get("recovery").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
934 let bytes = match ctx.version {
935 GtpVersion::V1 => encode_v1(
936 codec::V1_ECHO_RESPONSE,
937 0,
938 sequence,
939 &[ie(codec::V1_IE_RECOVERY, vec![recovery])],
940 ),
941 GtpVersion::V2 => GtpV2Message {
942 header: GtpV2Header::without_teid(codec::V2_ECHO_RESPONSE, sequence),
943 ies: vec![GtpV2Ie::new(codec::V2_IE_RECOVERY, 0, vec![recovery])],
944 }
945 .encode(),
946 };
947 Ok(Some((Decision::ModelEcho, Some((bytes, ctx.plane)))))
948 }
949
950 actions::RESULT_CREATE_RESPONSE
951 | actions::RESULT_UPDATE_RESPONSE
952 | actions::RESULT_DELETE_RESPONSE => {
953 let (cause, accepts) = cause_for(ctx.version, data)?;
954 let decision = if accepts {
955 Decision::ModelAccept
956 } else {
957 Decision::ModelReject
958 };
959 let bytes =
960 Self::encode_session_response(shared, ctx, name, sequence, teid, cause, data)?;
961 Ok(Some((decision, Some((bytes, Plane::Control)))))
962 }
963
964 actions::RESULT_ERROR_INDICATION => {
965 let target = data
966 .get("teid")
967 .and_then(|v| v.as_u64())
968 .context("gtp_error_indication result carries no teid")?
969 as u32;
970 let mut ies = vec![ie(codec::V1_IE_TEID_DATA_I, target.to_be_bytes().to_vec())];
974 ies.push(gsn_address_ie(shared.advertised_user_ip()));
975 let bytes = encode_v1(codec::V1_ERROR_INDICATION, 0, sequence, &ies);
976 Ok(Some((
977 Decision::ModelErrorIndication,
978 Some((bytes, Plane::User)),
979 )))
980 }
981
982 actions::RESULT_GPDU => {
983 let target =
984 data.get("teid")
985 .and_then(|v| v.as_u64())
986 .context("gtp_gpdu result carries no teid")? as u32;
987 let payload = hex::decode(
988 data.get("payload_hex")
989 .and_then(|v| v.as_str())
990 .unwrap_or_default(),
991 )
992 .context("gtp_gpdu payload is not valid hex")?;
993 if payload.len() > codec::MAX_GPDU_PAYLOAD_LEN {
994 anyhow::bail!(
995 "G-PDU payload is {} octets, over the {}-octet limit. GTP-U's Length \
996 field is 16 bits and the datagram has to fit one UDP packet, so a \
997 larger T-PDU would wrap the length field and then fail to send at \
998 all. A T-PDU is one IP packet — send several G-PDUs instead.",
999 payload.len(),
1000 codec::MAX_GPDU_PAYLOAD_LEN
1001 );
1002 }
1003 let header = match data.get("sequence") {
1007 Some(serde_json::Value::Null) | None => {
1008 GtpV1Header::new(codec::V1_G_PDU, target)
1009 }
1010 Some(_) => {
1011 let seq = Self::checked_sequence(data, GtpVersion::V1, ctx.sequence)?;
1012 GtpV1Header::with_sequence(codec::V1_G_PDU, target, seq as u16)
1013 }
1014 };
1015 let bytes = GtpV1Message {
1016 header,
1017 body: payload,
1018 }
1019 .encode();
1020 Ok(Some((Decision::ModelGpdu, Some((bytes, Plane::User)))))
1021 }
1022
1023 _ => Ok(None),
1024 }
1025 }
1026
1027 fn encode_session_response(
1028 shared: &Shared,
1029 ctx: &RequestContext,
1030 result_name: &str,
1031 sequence: u32,
1032 teid: u32,
1033 cause: u8,
1034 data: &serde_json::Value,
1035 ) -> Result<Vec<u8>> {
1036 let accepts = codec::cause_accepts(ctx.version, cause);
1037 let assigned = json_ip(data, "assigned_address")?;
1038 let control_teid = json_u32(data, "control_teid");
1039 let data_teid = json_u32(data, "data_teid");
1040 let charging_id = json_u32(data, "charging_id");
1041 let recovery = data
1042 .get("recovery")
1043 .and_then(|v| v.as_u64())
1044 .map(|v| v as u8);
1045 let dns: Vec<IpAddr> = data
1046 .get("dns_servers")
1047 .and_then(|v| v.as_array())
1048 .map(|a| {
1049 a.iter()
1050 .filter_map(|e| e.as_str())
1051 .filter_map(|s| s.parse().ok())
1052 .collect()
1053 })
1054 .unwrap_or_default();
1055
1056 let is_delete = result_name == actions::RESULT_DELETE_RESPONSE;
1057
1058 match ctx.version {
1059 GtpVersion::V1 => {
1060 let message_type = match result_name {
1061 actions::RESULT_CREATE_RESPONSE => codec::V1_CREATE_PDP_CONTEXT_RESPONSE,
1062 actions::RESULT_UPDATE_RESPONSE => codec::V1_UPDATE_PDP_CONTEXT_RESPONSE,
1063 _ => codec::V1_DELETE_PDP_CONTEXT_RESPONSE,
1064 };
1065
1066 let mut ies = vec![ie(codec::V1_IE_CAUSE, vec![cause])];
1068 if !is_delete && accepts {
1069 ies.push(ie(codec::V1_IE_REORDERING_REQUIRED, vec![0xFE]));
1072 }
1073 if let Some(r) = recovery {
1074 ies.push(ie(codec::V1_IE_RECOVERY, vec![r]));
1075 }
1076 if !is_delete {
1077 if let Some(t) = data_teid {
1078 ies.push(ie(codec::V1_IE_TEID_DATA_I, t.to_be_bytes().to_vec()));
1079 }
1080 if let Some(t) = control_teid {
1081 ies.push(ie(
1082 codec::V1_IE_TEID_CONTROL_PLANE,
1083 t.to_be_bytes().to_vec(),
1084 ));
1085 }
1086 if let Some(c) = charging_id {
1087 ies.push(ie(codec::V1_IE_CHARGING_ID, c.to_be_bytes().to_vec()));
1088 }
1089 if let Some(addr) = assigned {
1090 ies.push(ie(
1091 codec::V1_IE_END_USER_ADDRESS,
1092 codec::encode_end_user_address(Some(addr)),
1093 ));
1094 }
1095 }
1096 if !dns.is_empty() {
1097 ies.push(ie(
1098 codec::V1_IE_PROTOCOL_CONFIG_OPTIONS,
1099 codec::encode_pco_dns(&dns),
1100 ));
1101 }
1102 if !is_delete && accepts {
1103 ies.push(gsn_address_ie(shared.advertised_control_ip()));
1104 ies.push(gsn_address_ie(shared.advertised_user_ip()));
1105 }
1106
1107 Ok(encode_v1(message_type, teid, sequence, &ies))
1108 }
1109 GtpVersion::V2 => {
1110 let message_type = match result_name {
1111 actions::RESULT_CREATE_RESPONSE => codec::V2_CREATE_SESSION_RESPONSE,
1112 actions::RESULT_UPDATE_RESPONSE => codec::V2_MODIFY_BEARER_RESPONSE,
1113 _ => codec::V2_DELETE_SESSION_RESPONSE,
1114 };
1115
1116 let mut ies = vec![GtpV2Ie::new(codec::V2_IE_CAUSE, 0, vec![cause, 0])];
1118 if let Some(r) = recovery {
1119 ies.push(GtpV2Ie::new(codec::V2_IE_RECOVERY, 0, vec![r]));
1120 }
1121 if !is_delete {
1122 if !dns.is_empty() {
1123 ies.push(GtpV2Ie::new(
1124 codec::V2_IE_PCO,
1125 0,
1126 codec::encode_pco_dns(&dns),
1127 ));
1128 }
1129 if let Some(addr) = assigned {
1130 ies.push(GtpV2Ie::new(codec::V2_IE_PAA, 0, codec::encode_paa(addr)));
1131 }
1132 if let Some(t) = control_teid {
1133 ies.push(GtpV2Ie::new(
1135 codec::V2_IE_FTEID,
1136 1,
1137 codec::encode_fteid(7, t, shared.advertised_control_ip()),
1138 ));
1139 }
1140 if let Some(t) = data_teid {
1141 let inner = vec![
1144 GtpV2Ie::new(codec::V2_IE_EBI, 0, vec![ctx.bearer_id.unwrap_or(5)]),
1145 GtpV2Ie::new(codec::V2_IE_CAUSE, 0, vec![cause, 0]),
1146 GtpV2Ie::new(
1147 codec::V2_IE_FTEID,
1148 2,
1149 codec::encode_fteid(5, t, shared.advertised_user_ip()),
1150 ),
1151 ];
1152 ies.push(GtpV2Ie::new(
1153 codec::V2_IE_BEARER_CONTEXT,
1154 0,
1155 codec::encode_v2_grouped(&inner),
1156 ));
1157 }
1158 }
1159
1160 Ok(GtpV2Message {
1161 header: GtpV2Header::new(message_type, teid, sequence),
1162 ies,
1163 }
1164 .encode())
1165 }
1166 }
1167 }
1168
1169 fn synthesised_refusal(
1176 ctx: &RequestContext,
1177 category: WireFailure,
1178 ) -> Option<(Vec<u8>, Plane)> {
1179 let response_type = response_type(ctx.version, ctx.message_type)?;
1180
1181 let is_session_response = match ctx.version {
1184 GtpVersion::V1 => matches!(
1185 response_type,
1186 codec::V1_CREATE_PDP_CONTEXT_RESPONSE
1187 | codec::V1_UPDATE_PDP_CONTEXT_RESPONSE
1188 | codec::V1_DELETE_PDP_CONTEXT_RESPONSE
1189 ),
1190 GtpVersion::V2 => matches!(
1191 response_type,
1192 codec::V2_CREATE_SESSION_RESPONSE
1193 | codec::V2_MODIFY_BEARER_RESPONSE
1194 | codec::V2_DELETE_SESSION_RESPONSE
1195 ),
1196 };
1197 if !is_session_response {
1198 return None;
1199 }
1200
1201 let name = if category.is_overloaded() {
1202 "no_resources_available"
1203 } else {
1204 "system_failure"
1205 };
1206 let cause = codec::cause_by_name(name)?;
1207 debug_assert!(!cause.accepts, "a fail-closed cause must never accept");
1208
1209 let bytes = match ctx.version {
1210 GtpVersion::V1 => encode_v1(
1211 response_type,
1212 ctx.default_response_teid,
1213 ctx.sequence,
1214 &[ie(codec::V1_IE_CAUSE, vec![cause.v1])],
1215 ),
1216 GtpVersion::V2 => GtpV2Message {
1217 header: GtpV2Header::new(response_type, ctx.default_response_teid, ctx.sequence),
1218 ies: vec![GtpV2Ie::new(codec::V2_IE_CAUSE, 0, vec![cause.v2, 0])],
1219 }
1220 .encode(),
1221 };
1222 Some((bytes, Plane::Control))
1223 }
1224
1225 async fn record_connection(
1230 shared: &Shared,
1231 peer_addr: SocketAddr,
1232 plane: Plane,
1233 bytes: usize,
1234 ) -> ConnectionId {
1235 use crate::state::server::{
1236 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
1237 };
1238
1239 let connection_id = ConnectionId::new(shared.state.get_next_unified_id().await);
1240 let now = crate::utils::clock::Instant::now();
1241 shared
1242 .state
1243 .add_connection_to_server(
1244 shared.server_id,
1245 ServerConnectionState {
1246 id: connection_id,
1247 remote_addr: peer_addr,
1248 local_addr: shared.local_addr(plane),
1249 bytes_sent: 0,
1250 bytes_received: bytes as u64,
1251 packets_sent: 0,
1252 packets_received: 1,
1253 last_activity: now,
1254 status: ConnectionStatus::Active,
1255 status_changed_at: now,
1256 protocol_info: ProtocolConnectionInfo::empty(),
1257 },
1258 )
1259 .await;
1260 let _ = shared.status_tx.send("__UPDATE_UI__".to_string());
1261 connection_id
1262 }
1263
1264 async fn send(
1265 shared: &Shared,
1266 bytes: &[u8],
1267 peer_addr: SocketAddr,
1268 plane: Plane,
1269 connection_id: ConnectionId,
1270 ) {
1271 if plane == Plane::User && shared.user_socket.is_none() {
1272 warn!(
1273 "GTP wanted to send {} octets on the user plane to {peer_addr}, but the user \
1274 plane is disabled; sending on the control plane instead",
1275 bytes.len()
1276 );
1277 }
1278 let socket = shared.socket_for(plane);
1279 if let Err(e) = socket.send_to(bytes, peer_addr).await {
1280 Log::new(Some(&shared.status_tx)).error(format!("GTP send to {peer_addr} failed: {e}"));
1281 return;
1282 }
1283
1284 shared
1285 .state
1286 .update_connection_stats(
1287 shared.server_id,
1288 connection_id,
1289 None,
1290 Some(bytes.len() as u64),
1291 None,
1292 Some(1),
1293 )
1294 .await;
1295
1296 let log = Log::new(Some(&shared.status_tx));
1297 log.debug(format!(
1298 "GTP sent {} octets to {peer_addr} on the {} plane",
1299 bytes.len(),
1300 plane.as_str()
1301 ));
1302 log.trace(format!("GTP sent (hex): {}", hex::encode(bytes)));
1303 let _ = shared
1304 .status_tx
1305 .send(format!("→ GTP response to {peer_addr}"));
1306 }
1307}
1308
1309fn ie(ie_type: u8, value: Vec<u8>) -> GtpV1Ie {
1314 GtpV1Ie { ie_type, value }
1315}
1316
1317fn gsn_address_ie(addr: IpAddr) -> GtpV1Ie {
1318 let value = match addr {
1319 IpAddr::V4(v4) => v4.octets().to_vec(),
1320 IpAddr::V6(v6) => v6.octets().to_vec(),
1321 };
1322 ie(codec::V1_IE_GSN_ADDRESS, value)
1323}
1324
1325fn gsn_address(value: &[u8]) -> Option<IpAddr> {
1327 match value.len() {
1328 4 => Some(IpAddr::V4(Ipv4Addr::new(
1329 value[0], value[1], value[2], value[3],
1330 ))),
1331 16 => {
1332 let mut octets = [0u8; 16];
1333 octets.copy_from_slice(value);
1334 Some(IpAddr::V6(Ipv6Addr::from(octets)))
1335 }
1336 _ => None,
1337 }
1338}
1339
1340fn encode_v1(message_type: u8, teid: u32, sequence: u32, ies: &[GtpV1Ie]) -> Vec<u8> {
1343 GtpV1Message {
1344 header: GtpV1Header::with_sequence(message_type, teid, sequence as u16),
1345 body: codec::encode_v1_ies(ies),
1346 }
1347 .encode()
1348}
1349
1350fn response_type(version: GtpVersion, request_type: u8) -> Option<u8> {
1352 Some(match (version, request_type) {
1353 (GtpVersion::V1, codec::V1_ECHO_REQUEST) => codec::V1_ECHO_RESPONSE,
1354 (GtpVersion::V1, codec::V1_CREATE_PDP_CONTEXT_REQUEST) => {
1355 codec::V1_CREATE_PDP_CONTEXT_RESPONSE
1356 }
1357 (GtpVersion::V1, codec::V1_UPDATE_PDP_CONTEXT_REQUEST) => {
1358 codec::V1_UPDATE_PDP_CONTEXT_RESPONSE
1359 }
1360 (GtpVersion::V1, codec::V1_DELETE_PDP_CONTEXT_REQUEST) => {
1361 codec::V1_DELETE_PDP_CONTEXT_RESPONSE
1362 }
1363 (GtpVersion::V2, codec::V2_ECHO_REQUEST) => codec::V2_ECHO_RESPONSE,
1364 (GtpVersion::V2, codec::V2_CREATE_SESSION_REQUEST) => codec::V2_CREATE_SESSION_RESPONSE,
1365 (GtpVersion::V2, codec::V2_MODIFY_BEARER_REQUEST) => codec::V2_MODIFY_BEARER_RESPONSE,
1366 (GtpVersion::V2, codec::V2_DELETE_SESSION_REQUEST) => codec::V2_DELETE_SESSION_RESPONSE,
1367 _ => return None,
1368 })
1369}
1370
1371fn cause_for(version: GtpVersion, data: &serde_json::Value) -> Result<(u8, bool)> {
1373 let cause = data
1374 .get("cause")
1375 .context("session response result carries no cause")?;
1376 let key = match version {
1377 GtpVersion::V1 => "v1",
1378 GtpVersion::V2 => "v2",
1379 };
1380 let value = cause
1381 .get(key)
1382 .and_then(|v| v.as_u64())
1383 .with_context(|| format!("session response cause has no {key} code"))?
1384 as u8;
1385 Ok((value, codec::cause_accepts(version, value)))
1386}
1387
1388fn json_u32(data: &serde_json::Value, key: &str) -> Option<u32> {
1389 data.get(key).and_then(|v| v.as_u64()).map(|v| v as u32)
1390}
1391
1392fn json_ip(data: &serde_json::Value, key: &str) -> Result<Option<IpAddr>> {
1393 match data.get(key).and_then(|v| v.as_str()) {
1394 None => Ok(None),
1395 Some(s) => s
1396 .parse::<IpAddr>()
1397 .map(Some)
1398 .with_context(|| format!("'{key}' is not an IP address: {s:?}")),
1399 }
1400}