Skip to main content

netget/server/h323/
endpoint.rs

1//! H.323 endpoint: H.225.0 call signalling (Q.931 over TPKT) on TCP, the called side.
2//!
3//! One connection carries one call. The caller's Setup is decoded — numbers, aliases, display,
4//! the caller's endpoint and vendor, its fastStart proposals and whether it tunnels H.245 — and
5//! raised as `h323_setup`. The model answers with any of CallProceeding, Alerting, Connect,
6//! Release Complete or a forward (Facility `callForwarded`, then Release Complete). NetGet
7//! owns the rest: the call reference and its flag, the call and conference identifiers, the
8//! fastStart answer, tunnelled H.245 (capability exchange, master-slave determination, logical
9//! channels, end session) and the RTP/RTCP sockets the call advertises.
10//!
11//! ## Failing closed
12//!
13//! A Setup the model does not answer is never connected:
14//!
15//! - backend failure → Release Complete, cause 41 (temporary failure) or 42 (congestion) when
16//!   overloaded, `decision=fail_closed_llm_*`;
17//! - an answer with no call-control action → Release Complete cause 31, `decision=model_silent`;
18//! - an answer that alerts but never connects → Release Complete cause 19 (no answer) after
19//!   [`RING_TIMEOUT`];
20//! - an action NetGet cannot carry out → logged `decision=fail_closed_bad_action`, and the call
21//!   is released unless another action in the answer settled it.
22//!
23//! A malformed message, a message before Setup or a second Setup ends the call with Release
24//! Complete cause 111 (protocol error) or `newConnectionNeeded`, before any model call.
25
26use 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
47/// How long a new connection has to send its Setup.
48pub const FIRST_MESSAGE_TIMEOUT: Duration = Duration::from_secs(15);
49/// Signalling silence a call may keep (a connected call is quiet on this connection while the
50/// parties talk) before NetGet drops it.
51pub const CALL_IDLE_TIMEOUT: Duration = Duration::from_secs(3600);
52/// How long a call may ring (Alerting sent, no Connect) before it is released, cause 19.
53pub const RING_TIMEOUT: Duration = Duration::from_secs(120);
54/// Concurrent call-signalling connections.
55pub const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
56/// Queued messages from the reader while the model decides.
57const INBOUND_QUEUE: usize = 16;
58/// User-input events raised per call.
59const 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
177/// Release Complete on a connection that has no call yet (no Setup, or a bad one).
178async 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    // Tunnelled H.245 may already ride in the Setup (H.245 in Setup / parallel).
275    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
459/// Carry out the model's call-control commands in order. `Some(reason)` when the call ended.
460async 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}