1use super::actions::{self as act, CallCommand};
27use super::call::{self, cause, CallLeg, CallState, Incoming, MediaSockets, Role};
28use super::q931::{self, ie, msg, Q931};
29use super::{h225, h245};
30use crate::llm::action_helper::call_llm;
31use crate::llm::actions::protocol_trait::ActionResult;
32use crate::llm::ollama_client::OllamaClient;
33use crate::logging::emit::Log;
34use crate::protocol::Event;
35use crate::server::connection::ConnectionId;
36use crate::state::app_state::AppState;
37use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
38use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
39use crate::state::ServerId;
40use anyhow::Result;
41use serde_json::{json, Value};
42use std::net::SocketAddr;
43use std::sync::Arc;
44use std::time::Duration;
45use tokio::sync::mpsc;
46
47pub const FIRST_MESSAGE_TIMEOUT: Duration = Duration::from_secs(15);
49pub const CALL_IDLE_TIMEOUT: Duration = Duration::from_secs(3600);
52pub const RING_TIMEOUT: Duration = Duration::from_secs(120);
54pub const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
56const INBOUND_QUEUE: usize = 16;
58const MAX_USER_INPUT_EVENTS: usize = 32;
60
61#[derive(Clone, Debug)]
62pub struct EndpointConfig {
63 pub codecs: Vec<h245::Codec>,
64}
65
66struct Shared {
67 config: EndpointConfig,
68 app_state: Arc<AppState>,
69 llm_client: OllamaClient,
70 status_tx: mpsc::UnboundedSender<String>,
71 server_id: ServerId,
72}
73
74pub async fn spawn(
75 listen_addr: SocketAddr,
76 llm_client: OllamaClient,
77 app_state: Arc<AppState>,
78 status_tx: mpsc::UnboundedSender<String>,
79 server_id: ServerId,
80 config: EndpointConfig,
81) -> Result<SocketAddr> {
82 let listener = crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
83 let local_addr = listener.local_addr()?;
84 Log::new(Some(&status_tx)).info(format!(
85 "H.323 endpoint (H.225.0 call signalling) listening on {local_addr}"
86 ));
87 let shared = Arc::new(Shared {
88 config,
89 app_state: app_state.clone(),
90 llm_client,
91 status_tx: status_tx.clone(),
92 server_id,
93 });
94 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
95 let registrar = app_state.clone();
96 let accept = tokio::spawn(async move {
97 loop {
98 match crate::server::accept_bounded::accept_bounded(
99 &listener,
100 &limiter,
101 b"",
102 "H.323",
103 Some(&status_tx),
104 )
105 .await
106 {
107 Ok((stream, remote, permit)) => {
108 let shared = shared.clone();
109 let local = stream.local_addr().unwrap_or(local_addr);
110 app_state
111 .spawn_server_task(server_id, async move {
112 let _permit = permit;
113 handle_connection(shared, stream, remote, local).await;
114 })
115 .await;
116 }
117 Err(e) => {
118 Log::new(Some(&status_tx))
119 .error(format!("H.323 accept failed, stopping loop: {e}"));
120 break;
121 }
122 }
123 }
124 });
125 registrar.register_server_task(server_id, accept).await;
126 Ok(local_addr)
127}
128
129async fn handle_connection(
130 shared: Arc<Shared>,
131 stream: tokio::net::TcpStream,
132 remote: SocketAddr,
133 local: SocketAddr,
134) {
135 let app_state = shared.app_state.clone();
136 let server_id = shared.server_id;
137 let id = ConnectionId::new(app_state.get_next_unified_id().await);
138 let now = crate::utils::clock::Instant::now();
139 app_state
140 .add_connection_to_server(
141 server_id,
142 ConnectionState {
143 id,
144 remote_addr: remote,
145 local_addr: local,
146 bytes_sent: 0,
147 bytes_received: 0,
148 packets_sent: 0,
149 packets_received: 0,
150 last_activity: now,
151 status: ConnectionStatus::Active,
152 status_changed_at: now,
153 protocol_info: ProtocolConnectionInfo::empty(),
154 },
155 )
156 .await;
157 let _ = shared.status_tx.send("__UPDATE_UI__".to_string());
158 let _ = stream.set_nodelay(true);
159 let (read_half, write_half) = stream.into_split();
160 let reader = crate::server::accept_bounded::IdleTimeoutReader::with_first(
161 read_half,
162 FIRST_MESSAGE_TIMEOUT,
163 CALL_IDLE_TIMEOUT,
164 );
165 let (tx, rx) = mpsc::channel(INBOUND_QUEUE);
166 let reader_task = app_state
167 .spawn_server_task(server_id, call::read_loop(reader, tx))
168 .await;
169 let reason = run(&shared, id, remote, local, write_half, rx).await;
170 reader_task.abort();
171 Log::new(Some(&shared.status_tx)).info(format!("H.323 {id} from {remote}: {reason}"));
172 app_state.remove_peer_handle(server_id, id.as_u32()).await;
173 app_state.remove_connection_from_server(server_id, id).await;
174 let _ = shared.status_tx.send("__UPDATE_UI__".to_string());
175}
176
177async fn refuse_raw(writer: &mut tokio::net::tcp::OwnedWriteHalf, crv: u16, c: u8) {
179 use tokio::io::AsyncWriteExt;
180 let body = json!({"protocolIdentifier": h225::PROTOCOL_ID});
181 if let Ok(uu) = h225::encode_user_information("releaseComplete", body, false, &[]) {
182 let m = Q931::new(msg::RELEASE_COMPLETE, crv, true)
183 .with_ie(ie::CAUSE, q931::cause_ie(c))
184 .with_user_user(&uu);
185 if let Ok(bytes) = m.encode().and_then(|b| q931::tpkt(&b)) {
186 let _ = writer.write_all(&bytes).await;
187 }
188 }
189 let _ = writer.shutdown().await;
190}
191
192async fn run(
193 shared: &Shared,
194 id: ConnectionId,
195 remote: SocketAddr,
196 local: SocketAddr,
197 mut writer: tokio::net::tcp::OwnedWriteHalf,
198 mut rx: mpsc::Receiver<Result<Q931>>,
199) -> String {
200 let log = Log::new(Some(&shared.status_tx));
201 let setup = match rx.recv().await {
202 None => return "closed before a Setup".into(),
203 Some(Err(e)) => {
204 log.warn(format!(
205 "H.323 {id} from {remote}: no Setup decision=protocol_error: {e:#}"
206 ));
207 return format!("no Setup: {e:#}");
208 }
209 Some(Ok(m)) => m,
210 };
211 if setup.message_type != msg::SETUP {
212 log.warn(format!(
213 "H.323 {id} from {remote}: {} before Setup decision=protocol_error",
214 q931::message_name(setup.message_type)
215 ));
216 refuse_raw(&mut writer, setup.crv, cause::PROTOCOL_ERROR).await;
217 return "a message before Setup".into();
218 }
219 let ui = match setup
220 .user_user()
221 .and_then(|u| u.ok_or_else(|| anyhow::anyhow!("Setup without a User-user IE")))
222 .and_then(h225::decode_user_information)
223 {
224 Ok(ui) if ui.kind == "setup" => ui,
225 Ok(ui) => {
226 log.warn(format!(
227 "H.323 {id} from {remote}: Setup carries a {} UUIE decision=protocol_error",
228 ui.kind
229 ));
230 refuse_raw(&mut writer, setup.crv, cause::PROTOCOL_ERROR).await;
231 return "a Setup with the wrong UUIE".into();
232 }
233 Err(e) => {
234 log.warn(format!(
235 "H.323 {id} from {remote}: Setup not decodable decision=protocol_error: {e:#}"
236 ));
237 refuse_raw(&mut writer, setup.crv, cause::PROTOCOL_ERROR).await;
238 return format!("an undecodable Setup: {e:#}");
239 }
240 };
241 let setup_bytes = setup.encode().map(|b| b.len() as u64 + 4).unwrap_or(0);
242 shared
243 .app_state
244 .update_connection_stats(shared.server_id, id, Some(setup_bytes), None, Some(1), None)
245 .await;
246 let media = match MediaSockets::bind(local.ip()).await {
247 Ok(m) => m,
248 Err(e) => {
249 log.error(format!(
250 "H.323 {id}: no RTP ports decision=fail_closed_media: {e:#}"
251 ));
252 refuse_raw(&mut writer, setup.crv, cause::TEMPORARY_FAILURE).await;
253 return "no RTP ports".into();
254 }
255 };
256 let call_id = ui.call_identifier().unwrap_or_else(h225::new_guid);
257 let conference_id = ui
258 .body
259 .get("conferenceID")
260 .and_then(|c| c.as_str())
261 .map(str::to_string)
262 .unwrap_or_else(h225::new_guid);
263 let mut leg = CallLeg::new(
264 writer,
265 setup.crv,
266 Role::Callee,
267 call_id,
268 conference_id,
269 ui.h245_tunnelling(),
270 media,
271 shared.config.codecs.clone(),
272 );
273 leg.offered_fast_start = ui.fast_start();
274 for t in ui.h245_control() {
276 let _ = leg.h245.on_message(&t);
277 }
278
279 let proposals: Vec<Value> = leg
280 .offered_fast_start
281 .iter()
282 .filter_map(|p| h245::decode_olc(p).ok())
283 .map(|o| h245::describe_proposal(&o))
284 .collect();
285 let event_data = json!({
286 "remote": remote.to_string(),
287 "crv": setup.crv,
288 "call_id": leg.call_id,
289 "conference_id": leg.conference_id,
290 "calling_number": setup.number(ie::CALLING_PARTY_NUMBER),
291 "called_number": setup.number(ie::CALLED_PARTY_NUMBER),
292 "display": setup.display(),
293 "source_aliases": h225::aliases_text(ui.body.get("sourceAddress")),
294 "destination_aliases": h225::aliases_text(ui.body.get("destinationAddress")),
295 "source_endpoint": h225::endpoint_summary(ui.body.get("sourceInfo")),
296 "source_call_signal_address": ui.body.get("sourceCallSignalAddress").map(h225::transport_text),
297 "fast_start": proposals,
298 "h245_tunnelling": ui.h245_tunnelling(),
299 "h245_address": ui.body.get("h245Address").map(h225::transport_text),
300 "endpoint_identifier": ui.body.get("endpointIdentifier"),
301 });
302 log.info(format!(
303 "H.323 {id}: Setup from {remote} (crv {}, {} → {})",
304 setup.crv,
305 event_data["calling_number"].as_str().unwrap_or("-"),
306 event_data["called_number"].as_str().unwrap_or("-")
307 ));
308 let mut peer_rx = crate::server::peer_support::register_peer_channel(
309 &shared.app_state,
310 shared.server_id,
311 id.as_u32(),
312 )
313 .await;
314
315 let decision = consult(shared, id, &Event::new(&act::SETUP_EVENT, event_data)).await;
316 let mut ringing_since: Option<tokio::time::Instant> = None;
317 match decision {
318 Decision::Failed { overloaded } => {
319 let c = if overloaded {
320 cause::CONGESTION
321 } else {
322 cause::TEMPORARY_FAILURE
323 };
324 let _ = leg.release(c, Some("undefinedReason")).await;
325 return "Setup not answered (backend failure)".into();
326 }
327 Decision::Answered { commands, refused } => {
328 if commands.is_empty() {
329 let tag = if refused.is_empty() {
330 "model_silent"
331 } else {
332 "fail_closed_bad_action"
333 };
334 log.warn(format!(
335 "H.323 {id}: Setup answered with no call-control action decision={tag}{}; releasing, cause 31",
336 if refused.is_empty() { String::new() } else { format!(" ({})", refused.join("; ")) }
337 ));
338 let _ = leg
339 .release(cause::NORMAL_UNSPECIFIED, Some("undefinedReason"))
340 .await;
341 return "Setup not answered".into();
342 }
343 let rejects = commands
344 .iter()
345 .any(|c| matches!(c, CallCommand::Release { .. } | CallCommand::Forward { .. }));
346 log.info(format!(
347 "H.323 {id}: Setup decision={}",
348 if rejects {
349 "model_reject"
350 } else {
351 "model_answer"
352 }
353 ));
354 if let Some(end) = carry_out(shared, id, &mut leg, commands).await {
355 return end;
356 }
357 }
358 }
359 if leg.state == CallState::Released {
360 return "released".into();
361 }
362 if matches!(
363 leg.state,
364 CallState::Offered | CallState::Proceeding | CallState::Alerting
365 ) {
366 ringing_since = Some(tokio::time::Instant::now());
367 }
368
369 let mut peer_open = true;
370 let mut user_input_seen = 0usize;
371 loop {
372 let ring_deadline = ringing_since.map(|t| t + RING_TIMEOUT);
373 tokio::select! {
374 m = rx.recv() => {
375 let m = match m {
376 None => return format!("the caller closed the connection (call {})", leg.state.name()),
377 Some(Err(e)) => {
378 log.warn(format!("H.323 {id}: decision=protocol_error: {e:#}"));
379 let _ = leg.release(cause::PROTOCOL_ERROR, None).await;
380 return format!("protocol error: {e:#}");
381 }
382 Some(Ok(m)) => m,
383 };
384 let received = m.encode().map(|b| b.len() as u64 + 4).unwrap_or(0);
385 let sent_before = leg.bytes_sent;
386 let outcome = leg.on_incoming(&m).await;
387 shared.app_state
388 .update_connection_stats(shared.server_id, id, Some(received), Some(leg.bytes_sent - sent_before), Some(1), None)
389 .await;
390 match outcome {
391 Ok(Incoming::Released { cause: c, reason }) => {
392 log.info(format!("H.323 {id}: call ended by the caller: {}", json!({"cause": c, "reason": reason, "call": leg.summary()})));
393 return format!("the caller released (cause {c:?}, {reason:?})");
394 }
395 Ok(Incoming::WrongCallReference { crv, from_destination }) => {
396 log.warn(format!(
397 "H.323 {id}: {} for call reference {crv} (flag {from_destination}) on call {} decision=protocol_error; releasing, cause 81",
398 q931::message_name(m.message_type), leg.crv
399 ));
400 let _ = leg.release(cause::INVALID_CALL_REFERENCE, None).await;
401 return "a message for another call reference".into();
402 }
403 Ok(Incoming::UnexpectedSetup) => {
404 log.warn(format!("H.323 {id}: a second Setup on this connection decision=protocol_error"));
405 let _ = leg.release(cause::NORMAL_UNSPECIFIED, Some("newConnectionNeeded")).await;
406 return "a second Setup".into();
407 }
408 Ok(_) => {}
409 Err(e) => {
410 log.warn(format!("H.323 {id}: {} not decodable decision=protocol_error: {e:#}", q931::message_name(m.message_type)));
411 }
412 }
413 if leg.h245.ended && leg.state != CallState::Released {
414 let _ = leg.release(cause::NORMAL_CLEARING, None).await;
415 log.info(format!("H.323 {id}: call ended by the caller's endSessionCommand: {}", leg.summary()));
416 return "the caller ended the H.245 session".into();
417 }
418 while user_input_seen < leg.h245.user_input.len() && user_input_seen < MAX_USER_INPUT_EVENTS {
419 let input = leg.h245.user_input[user_input_seen].clone();
420 user_input_seen += 1;
421 let event = Event::new(&act::USER_INPUT_EVENT, json!({
422 "input": input,
423 "call_id": leg.call_id,
424 "state": leg.state.name(),
425 }));
426 if let Decision::Answered { commands, .. } = consult(shared, id, &event).await {
427 if let Some(end) = carry_out(shared, id, &mut leg, commands).await {
428 return end;
429 }
430 }
431 }
432 if leg.state == CallState::Connected {
433 ringing_since = None;
434 }
435 }
436 _ = async { tokio::time::sleep_until(ring_deadline.expect("guarded")).await }, if ring_deadline.is_some() => {
437 log.info(format!("H.323 {id}: rang for {}s without a Connect decision=fail_closed_ring_timeout; releasing, cause 19", RING_TIMEOUT.as_secs()));
438 let _ = leg.release(cause::NO_ANSWER, None).await;
439 return "rang without being answered".into();
440 }
441 c = peer_rx.recv(), if peer_open => {
442 let Some(c) = c else {
443 peer_open = false;
444 continue;
445 };
446 if let Some(end) = inject(shared, id, &mut leg, c).await {
447 return end;
448 }
449 if leg.state == CallState::Connected {
450 ringing_since = None;
451 } else if ringing_since.is_none() && leg.state != CallState::Released {
452 ringing_since = Some(tokio::time::Instant::now());
453 }
454 }
455 }
456 }
457}
458
459async fn carry_out(
461 shared: &Shared,
462 id: ConnectionId,
463 leg: &mut CallLeg,
464 commands: Vec<CallCommand>,
465) -> Option<String> {
466 let log = Log::new(Some(&shared.status_tx));
467 for c in commands {
468 let what = c.name();
469 let before = leg.bytes_sent;
470 let result = apply(leg, c).await;
471 if leg.bytes_sent > before {
472 shared
473 .app_state
474 .update_connection_stats(
475 shared.server_id,
476 id,
477 None,
478 Some(leg.bytes_sent - before),
479 None,
480 Some(1),
481 )
482 .await;
483 }
484 match result {
485 Ok(()) if leg.state == CallState::Connected => log.info(format!(
486 "H.323 {id}: {what} sent; media {}",
487 leg.media_json()
488 )),
489 Ok(()) => log.info(format!("H.323 {id}: {what} sent")),
490 Err(e) => {
491 log.error(format!(
492 "H.323 {id}: {what} refused decision=fail_closed_bad_action: {e:#}"
493 ));
494 if leg.state != CallState::Connected && leg.state != CallState::Released {
495 let _ = leg
496 .release(cause::TEMPORARY_FAILURE, Some("undefinedReason"))
497 .await;
498 }
499 }
500 }
501 if leg.state == CallState::Released {
502 log.info(format!(
503 "H.323 {id}: call ended by the model: {}",
504 leg.summary()
505 ));
506 return Some(format!("released by the model ({what})"));
507 }
508 }
509 None
510}
511
512async fn apply(leg: &mut CallLeg, c: CallCommand) -> Result<()> {
513 match c {
514 CallCommand::Proceeding => leg.call_proceeding().await,
515 CallCommand::Alerting => leg.alerting().await,
516 CallCommand::Connect { codecs, display } => leg.connect(&codecs, display.as_deref()).await,
517 CallCommand::Release { cause, reason } => leg.release(cause, reason.as_deref()).await,
518 CallCommand::Forward { address, aliases } => leg.forward(address, aliases).await,
519 }
520}
521
522async fn inject(
523 shared: &Shared,
524 id: ConnectionId,
525 leg: &mut CallLeg,
526 command: ClientCommand,
527) -> Option<String> {
528 shared
529 .app_state
530 .record_access_log(
531 crate::state::AccessLogOwner::Server(shared.server_id.as_u32()),
532 "h323",
533 None,
534 "injected_action",
535 command.action.clone(),
536 vec![],
537 )
538 .await;
539 let (outcome, end) = match act::parse_call_command(&command.action) {
540 Err(e) => (
541 ClientSendOutcome::Rejected {
542 error: format!("{e:#}"),
543 },
544 None,
545 ),
546 Ok(c) => {
547 let what = c.name();
548 match apply(leg, c).await {
549 Ok(()) if leg.state == CallState::Released => {
550 Log::new(Some(&shared.status_tx)).info(format!(
551 "H.323 {id}: call ended by the operator: {}",
552 leg.summary()
553 ));
554 (
555 ClientSendOutcome::Disconnected,
556 Some(format!("released by the operator ({what})")),
557 )
558 }
559 Ok(()) => (
560 ClientSendOutcome::Executed {
561 detail: format!("{what} sent; call {}", leg.state.name()),
562 },
563 None,
564 ),
565 Err(e) => (
566 ClientSendOutcome::Rejected {
567 error: format!("{e:#}"),
568 },
569 None,
570 ),
571 }
572 }
573 };
574 Log::new(Some(&shared.status_tx)).info(format!("H.323 {id}: injected action: {outcome:?}"));
575 let _ = command.reply_tx.send(Ok(outcome));
576 end
577}
578
579enum Decision {
580 Failed {
581 overloaded: bool,
582 },
583 Answered {
584 commands: Vec<CallCommand>,
585 refused: Vec<String>,
586 },
587}
588
589async fn consult(shared: &Shared, id: ConnectionId, event: &Event) -> Decision {
590 let log = Log::new(Some(&shared.status_tx));
591 let execution = match call_llm(
592 &shared.llm_client,
593 &shared.app_state,
594 shared.server_id,
595 Some(id),
596 event,
597 &act::H323EndpointProtocol::new(),
598 )
599 .await
600 {
601 Ok(x) => x,
602 Err(e) => {
603 let overloaded = matches!(
604 crate::utils::wire_failure::WireFailure::classify(&e),
605 crate::utils::wire_failure::WireFailure::Overloaded
606 );
607 log.error(format!(
608 "H.323 {id}: {} not answered decision={}: {e}",
609 event.event_type.id,
610 if overloaded {
611 "fail_closed_llm_overloaded"
612 } else {
613 "fail_closed_llm_error"
614 }
615 ));
616 return Decision::Failed { overloaded };
617 }
618 };
619 for m in execution.messages {
620 let _ = shared.status_tx.send(m);
621 }
622 let mut commands = Vec::new();
623 let mut stack = execution.protocol_results;
624 stack.reverse();
625 while let Some(r) = stack.pop() {
626 match r {
627 ActionResult::Custom { data, .. } => {
628 if let Ok(c) = act::parse_call_command(&data) {
629 commands.push(c);
630 }
631 }
632 ActionResult::Multiple(inner) => stack.extend(inner.into_iter().rev()),
633 _ => {}
634 }
635 }
636 let refused = execution
637 .failures
638 .iter()
639 .map(|f| format!("{}: {}", f.action, f.error))
640 .collect();
641 Decision::Answered { commands, refused }
642}