Skip to main content

netget/server/h323/
gatekeeper.rs

1//! H.323 gatekeeper: H.225.0 RAS over UDP.
2//!
3//! NetGet decodes and encodes every RAS message and keeps the gatekeeper's runtime state —
4//! registrations (bounded by [`MAX_REGISTRATIONS`], each expiring at its time-to-live) and
5//! admitted calls (bounded by [`MAX_CALLS`]). Nothing is stored past the process.
6//!
7//! The model decides the policy:
8//!
9//! - **GRQ** → `h323_gatekeeper_request`: GCF or GRJ.
10//! - **RRQ** (full) → `h323_registration_request`: RCF with the aliases and time-to-live it
11//!   chooses, or RRJ. A lightweight RRQ (`keepAlive`) for a live registration is refreshed by
12//!   NetGet without asking; for an unknown endpoint it is RRJ `fullRegistrationRequired`.
13//! - **ARQ** → `h323_admission_request`: ACF naming the destination signalling address — a
14//!   registered endpoint's, chosen by alias, or an explicit `ip:port` — or ARJ.
15//! - **LRQ** → `h323_location_request`: LCF or LRJ.
16//!
17//! NetGet answers the rest itself: URQ (UCF / URJ `notCurrentlyRegistered`), DRQ (DCF / DRJ
18//! `notRegistered`), BRQ (BCF up to the configured bandwidth), IRR (IACK when asked for one),
19//! an ARQ from an endpoint that is not registered (ARJ `callerNotRegistered`), and a
20//! retransmitted request: the cached answer again, or RIP while the model is still deciding.
21//!
22//! ## Failing closed
23//!
24//! Every request that goes to the model is answered with a reject unless the model confirms
25//! it: a backend failure is `resourceUnavailable` (overloaded) or `undefinedReason`, tagged
26//! `decision=fail_closed_llm_*`; an answer with no confirm or reject is `undefinedReason`,
27//! `decision=model_silent`; an action NetGet cannot carry out (an alias another endpoint holds,
28//! a destination it cannot resolve) is the matching reject, `decision=fail_closed_bad_action`.
29//! Nothing here can admit or register on its own.
30
31use super::actions::{self as act, GkCommand};
32use super::h225;
33use crate::llm::action_helper::call_llm;
34use crate::llm::actions::protocol_trait::ActionResult;
35use crate::llm::ollama_client::OllamaClient;
36use crate::logging::emit::Log;
37use crate::protocol::Event;
38use crate::server::connection::ConnectionId;
39use crate::state::app_state::AppState;
40use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
41use crate::state::ServerId;
42use crate::utils::clock::Instant;
43use anyhow::Result;
44use serde_json::{json, Value};
45use std::collections::{HashMap, HashSet, VecDeque};
46use std::net::{IpAddr, SocketAddr};
47use std::sync::{Arc, Mutex};
48use std::time::Duration;
49use tokio::net::UdpSocket;
50use tokio::sync::mpsc;
51
52/// Largest RAS datagram read; anything longer is truncated by the read and refused by the
53/// decoder.
54pub const MAX_RAS_DATAGRAM: usize = 4096;
55/// Endpoints registered at once.
56pub const MAX_REGISTRATIONS: usize = 256;
57/// Calls admitted and not yet disengaged.
58pub const MAX_CALLS: usize = 512;
59/// Requests being decided by the model at once; past this a request is refused at once.
60pub const MAX_IN_FLIGHT: usize = 64;
61/// Answers remembered for retransmitted requests.
62const ANSWER_CACHE: usize = 256;
63/// How long a remembered answer is replayed for a retransmission.
64const ANSWER_TTL: Duration = Duration::from_secs(30);
65/// RequestInProgress delay sent while the model decides (milliseconds).
66const RIP_DELAY_MS: u64 = 15_000;
67/// Time-to-live when the model gives none and the endpoint asked for none.
68pub const DEFAULT_TTL_SECS: u64 = 300;
69/// The longest time-to-live NetGet grants.
70pub const MAX_TTL_SECS: u64 = 3600;
71/// How long past its time-to-live a registration survives (endpoints refresh late).
72const TTL_GRACE: Duration = Duration::from_secs(30);
73
74pub const ADMISSION_REASONS: &[&str] = &[
75    "calledPartyNotRegistered",
76    "invalidPermission",
77    "requestDenied",
78    "undefinedReason",
79    "callerNotRegistered",
80    "routeCallToGatekeeper",
81    "invalidEndpointIdentifier",
82    "resourceUnavailable",
83    "securityDenial",
84    "qosControlNotSupported",
85    "incompleteAddress",
86    "aliasesInconsistent",
87    "exceedsCallCapacity",
88    "collectDestination",
89    "collectPIN",
90    "noRouteToDestination",
91    "unallocatedNumber",
92];
93
94pub const REGISTRATION_REASONS: &[&str] = &[
95    "discoveryRequired",
96    "invalidRevision",
97    "invalidCallSignalAddress",
98    "invalidRASAddress",
99    "duplicateAlias",
100    "invalidTerminalType",
101    "undefinedReason",
102    "transportNotSupported",
103    "transportQOSNotSupported",
104    "resourceUnavailable",
105    "invalidAlias",
106    "securityDenial",
107    "fullRegistrationRequired",
108];
109
110pub const GATEKEEPER_REASONS: &[&str] = &[
111    "resourceUnavailable",
112    "terminalExcluded",
113    "invalidRevision",
114    "undefinedReason",
115    "securityDenial",
116];
117
118pub const LOCATION_REASONS: &[&str] = &[
119    "notRegistered",
120    "invalidPermission",
121    "requestDenied",
122    "undefinedReason",
123    "securityDenial",
124    "aliasesInconsistent",
125    "resourceUnavailable",
126    "hopCountExceeded",
127    "incompleteAddress",
128    "noRouteToDestination",
129    "unallocatedNumber",
130];
131
132/// Startup configuration.
133#[derive(Clone, Debug)]
134pub struct GatekeeperConfig {
135    pub gatekeeper_id: String,
136    pub default_ttl: u64,
137    pub max_bandwidth: u64,
138}
139
140#[derive(Clone, Debug)]
141struct Registration {
142    endpoint_id: String,
143    aliases: Vec<Value>,
144    call_signal: Vec<Value>,
145    ras: Vec<Value>,
146    ttl: u64,
147    expires: Instant,
148    endpoint: Value,
149}
150
151#[derive(Clone, Debug)]
152struct AdmittedCall {
153    endpoint_id: String,
154    bandwidth: u64,
155}
156
157#[derive(Default)]
158struct State {
159    registrations: HashMap<String, Registration>,
160    calls: HashMap<String, AdmittedCall>,
161    next_endpoint: u64,
162    answers: VecDeque<((SocketAddr, u16), Vec<u8>, Instant)>,
163    in_flight: HashSet<(SocketAddr, u16)>,
164}
165
166impl State {
167    fn expire(&mut self) {
168        let now = Instant::now();
169        self.registrations
170            .retain(|_, r| r.expires + TTL_GRACE > now);
171        let live: HashSet<String> = self.registrations.keys().cloned().collect();
172        self.calls.retain(|_, c| live.contains(&c.endpoint_id));
173        while self
174            .answers
175            .front()
176            .is_some_and(|(_, _, at)| now.duration_since(*at) > ANSWER_TTL)
177        {
178            self.answers.pop_front();
179        }
180    }
181
182    fn remember(&mut self, key: (SocketAddr, u16), answer: Vec<u8>) {
183        if self.answers.len() >= ANSWER_CACHE {
184            self.answers.pop_front();
185        }
186        self.answers.push_back((key, answer, Instant::now()));
187    }
188
189    fn cached(&self, key: (SocketAddr, u16)) -> Option<Vec<u8>> {
190        self.answers
191            .iter()
192            .rev()
193            .find(|(k, _, _)| *k == key)
194            .map(|(_, a, _)| a.clone())
195    }
196
197    /// The registration holding `alias`, if any.
198    fn holder_of(&self, alias: &Value) -> Option<&Registration> {
199        self.registrations
200            .values()
201            .find(|r| r.aliases.iter().any(|a| h225::same_alias(a, alias)))
202    }
203
204    fn by_call_signal(&self, addrs: &[Value]) -> Option<String> {
205        self.registrations
206            .values()
207            .find(|r| r.call_signal.iter().any(|a| addrs.contains(a)))
208            .map(|r| r.endpoint_id.clone())
209    }
210}
211
212struct Shared {
213    socket: Arc<UdpSocket>,
214    local: SocketAddr,
215    config: GatekeeperConfig,
216    state: Mutex<State>,
217    app_state: Arc<AppState>,
218    llm_client: OllamaClient,
219    status_tx: mpsc::UnboundedSender<String>,
220    server_id: ServerId,
221}
222
223impl Shared {
224    fn state(&self) -> std::sync::MutexGuard<'_, State> {
225        self.state.lock().unwrap_or_else(|p| p.into_inner())
226    }
227
228    fn log(&self) -> Log<'_> {
229        Log::new(Some(&self.status_tx))
230    }
231
232    /// This gatekeeper's RAS address as `peer` sees it: the bound address, or — when bound
233    /// to the unspecified address — the source address the route to `peer` uses.
234    fn ras_address_for(&self, peer: SocketAddr) -> SocketAddr {
235        if !self.local.ip().is_unspecified() {
236            return self.local;
237        }
238        let ip = route_source(peer).unwrap_or(self.local.ip());
239        SocketAddr::new(ip, self.local.port())
240    }
241}
242
243/// The local address the kernel would send from to reach `peer`.
244pub fn route_source(peer: SocketAddr) -> Option<IpAddr> {
245    let bind: SocketAddr = if peer.is_ipv4() {
246        "0.0.0.0:0".parse().ok()?
247    } else {
248        "[::]:0".parse().ok()?
249    };
250    let s = std::net::UdpSocket::bind(bind).ok()?;
251    s.connect(peer).ok()?;
252    s.local_addr().ok().map(|a| a.ip())
253}
254
255pub async fn spawn(
256    listen_addr: SocketAddr,
257    llm_client: OllamaClient,
258    app_state: Arc<AppState>,
259    status_tx: mpsc::UnboundedSender<String>,
260    server_id: ServerId,
261    config: GatekeeperConfig,
262) -> Result<SocketAddr> {
263    let socket = Arc::new(UdpSocket::bind(listen_addr).await?);
264    let local = socket.local_addr()?;
265    Log::new(Some(&status_tx)).info(format!(
266        "H.323 gatekeeper {} (RAS) listening on {local}",
267        config.gatekeeper_id
268    ));
269    let shared = Arc::new(Shared {
270        socket: socket.clone(),
271        local,
272        config,
273        state: Mutex::new(State::default()),
274        app_state: app_state.clone(),
275        llm_client,
276        status_tx: status_tx.clone(),
277        server_id,
278    });
279    let registrar = app_state.clone();
280    let task = tokio::spawn(async move {
281        let mut buf = vec![0u8; MAX_RAS_DATAGRAM];
282        loop {
283            let (n, peer) = match socket.recv_from(&mut buf).await {
284                Ok(x) => x,
285                Err(e) => {
286                    Log::new(Some(&status_tx))
287                        .error(format!("H.323 gatekeeper receive failed, stopping: {e}"));
288                    break;
289                }
290            };
291            let datagram = buf[..n].to_vec();
292            let shared = shared.clone();
293            app_state
294                .spawn_server_task(server_id, async move {
295                    handle_datagram(shared, datagram, peer).await;
296                })
297                .await;
298        }
299    });
300    registrar.register_server_task(server_id, task).await;
301    Ok(local)
302}
303
304async fn track(shared: &Shared, peer: SocketAddr, n: usize) -> ConnectionId {
305    let id = ConnectionId::new(shared.app_state.get_next_unified_id().await);
306    let now = Instant::now();
307    shared
308        .app_state
309        .add_connection_to_server(
310            shared.server_id,
311            ConnectionState {
312                id,
313                remote_addr: peer,
314                local_addr: shared.local,
315                bytes_sent: 0,
316                bytes_received: n as u64,
317                packets_sent: 0,
318                packets_received: 1,
319                last_activity: now,
320                status: ConnectionStatus::Active,
321                status_changed_at: now,
322                protocol_info: ProtocolConnectionInfo::empty(),
323            },
324        )
325        .await;
326    let _ = shared.status_tx.send("__UPDATE_UI__".to_string());
327    id
328}
329
330async fn send(
331    shared: &Shared,
332    id: ConnectionId,
333    peer: SocketAddr,
334    kind: &str,
335    body: Value,
336) -> Option<Vec<u8>> {
337    let bytes = match h225::encode_ras(kind, body) {
338        Ok(b) => b,
339        Err(e) => {
340            shared.log().error(format!(
341                "H.323 gatekeeper: could not encode {}: {e:#}",
342                h225::ras_abbreviation(kind)
343            ));
344            return None;
345        }
346    };
347    match shared.socket.send_to(&bytes, peer).await {
348        Ok(n) => {
349            shared
350                .app_state
351                .update_connection_stats(shared.server_id, id, None, Some(n as u64), None, Some(1))
352                .await;
353            shared.log().debug(format!(
354                "H.323 gatekeeper → {peer}: {}",
355                h225::ras_abbreviation(kind)
356            ));
357            Some(bytes)
358        }
359        Err(e) => {
360            shared
361                .log()
362                .error(format!("H.323 gatekeeper: send to {peer} failed: {e}"));
363            None
364        }
365    }
366}
367
368async fn handle_datagram(shared: Arc<Shared>, datagram: Vec<u8>, peer: SocketAddr) {
369    let id = track(&shared, peer, datagram.len()).await;
370    let (kind, body) = match h225::decode_ras(&datagram) {
371        Ok(x) => x,
372        Err(e) => {
373            shared.log().warn(format!(
374                "H.323 gatekeeper: {} octets from {peer} are not a RAS message decision=protocol_error: {e:#}",
375                datagram.len()
376            ));
377            return;
378        }
379    };
380    let abbrev = h225::ras_abbreviation(&kind);
381    let Some(seq) = h225::seq_num(&body) else {
382        shared.log().warn(format!(
383            "H.323 gatekeeper: {abbrev} from {peer} without a sequence number decision=protocol_error"
384        ));
385        return;
386    };
387    shared
388        .log()
389        .debug(format!("H.323 gatekeeper ← {peer}: {abbrev} seq {seq}"));
390    let key = (peer, seq);
391    let (cached, in_flight) = {
392        let mut st = shared.state();
393        st.expire();
394        (st.cached(key), st.in_flight.contains(&key))
395    };
396    if let Some(answer) = cached {
397        let _ = shared.socket.send_to(&answer, peer).await;
398        shared.log().debug(format!(
399            "H.323 gatekeeper: {abbrev} seq {seq} from {peer} retransmitted; answered again"
400        ));
401        return;
402    }
403    if in_flight {
404        send(
405            &shared,
406            id,
407            peer,
408            "requestInProgress",
409            json!({"requestSeqNum": seq, "delay": RIP_DELAY_MS}),
410        )
411        .await;
412        return;
413    }
414    let answer = match kind.as_str() {
415        "gatekeeperRequest" => gatekeeper_request(&shared, id, peer, seq, &body).await,
416        "registrationRequest" => registration_request(&shared, id, peer, seq, &body).await,
417        "unregistrationRequest" => unregistration_request(&shared, seq, &body),
418        "admissionRequest" => admission_request(&shared, id, peer, seq, &body).await,
419        "disengageRequest" => disengage_request(&shared, seq, &body),
420        "locationRequest" => location_request(&shared, id, peer, seq, &body).await,
421        "bandwidthRequest" => Some((
422            "bandwidthConfirm",
423            json!({
424                "requestSeqNum": seq,
425                "bandWidth": body.get("bandWidth").and_then(|b| b.as_u64()).unwrap_or(0).min(shared.config.max_bandwidth),
426            }),
427        )),
428        "infoRequestResponse" => {
429            let wants = body
430                .get("needResponse")
431                .and_then(|v| v.as_bool())
432                .unwrap_or(false);
433            wants.then(|| ("infoRequestAck", json!({"requestSeqNum": seq})))
434        }
435        "resourcesAvailableIndicate" => Some((
436            "resourcesAvailableConfirm",
437            json!({"requestSeqNum": seq, "protocolIdentifier": h225::PROTOCOL_ID}),
438        )),
439        other
440            if other.ends_with("Confirm")
441                || other.ends_with("Reject")
442                || other == "requestInProgress" =>
443        {
444            shared.log().debug(format!(
445                "H.323 gatekeeper: ignoring {abbrev} from {peer}: a gatekeeper sends these, it does not answer them"
446            ));
447            None
448        }
449        _ => {
450            shared.log().info(format!(
451                "H.323 gatekeeper: {abbrev} from {peer} is not handled; answering XRS"
452            ));
453            Some((
454                "unknownMessageResponse",
455                json!({"requestSeqNum": seq, "messageNotUnderstood": hex::encode(&datagram)}),
456            ))
457        }
458    };
459    if let Some((kind, body)) = answer {
460        if let Some(bytes) = send(&shared, id, peer, kind, body).await {
461            shared.state().remember(key, bytes);
462        }
463    }
464}
465
466/// Run one request past the model with the in-flight bound held. `None` when the bound is
467/// reached (the caller refuses at once).
468async fn consult(
469    shared: &Shared,
470    id: ConnectionId,
471    key: (SocketAddr, u16),
472    event: Event,
473) -> Option<Consulted> {
474    {
475        let mut st = shared.state();
476        if st.in_flight.len() >= MAX_IN_FLIGHT {
477            return None;
478        }
479        st.in_flight.insert(key);
480    }
481    let result = call_llm(
482        &shared.llm_client,
483        &shared.app_state,
484        shared.server_id,
485        Some(id),
486        &event,
487        &act::H323GatekeeperProtocol::new(),
488    )
489    .await;
490    shared.state().in_flight.remove(&key);
491    let execution = match result {
492        Ok(x) => x,
493        Err(e) => {
494            let overloaded = matches!(
495                crate::utils::wire_failure::WireFailure::classify(&e),
496                crate::utils::wire_failure::WireFailure::Overloaded
497            );
498            shared.log().error(format!(
499                "H.323 gatekeeper: {} from {} not answered decision={}: {e}",
500                event.event_type.id,
501                key.0,
502                if overloaded {
503                    "fail_closed_llm_overloaded"
504                } else {
505                    "fail_closed_llm_error"
506                }
507            ));
508            return Some(Consulted::Failed { overloaded });
509        }
510    };
511    for m in execution.messages {
512        let _ = shared.status_tx.send(m);
513    }
514    let mut commands = Vec::new();
515    let mut stack = execution.protocol_results;
516    stack.reverse();
517    while let Some(r) = stack.pop() {
518        match r {
519            ActionResult::Custom { data, .. } => {
520                if let Ok(c) = act::parse_gk_command(&data) {
521                    commands.push(c);
522                }
523            }
524            ActionResult::Multiple(inner) => stack.extend(inner.into_iter().rev()),
525            _ => {}
526        }
527    }
528    let refused: Vec<String> = execution
529        .failures
530        .iter()
531        .map(|f| format!("{}: {}", f.action, f.error))
532        .collect();
533    Some(Consulted::Answered { commands, refused })
534}
535
536enum Consulted {
537    Failed {
538        overloaded: bool,
539    },
540    Answered {
541        commands: Vec<GkCommand>,
542        refused: Vec<String>,
543    },
544}
545
546/// What the model decided about one request kind, after picking out the commands that answer it.
547enum Verdict<C, R> {
548    Confirm(C),
549    Reject(R),
550    /// No usable answer (already logged with its decision tag); the reject reason to send.
551    Refuse(&'static str),
552}
553
554fn verdict<C, R>(
555    shared: &Shared,
556    name: &str,
557    peer: SocketAddr,
558    consulted: Option<Consulted>,
559    pick: impl Fn(GkCommand) -> Option<Result<C, R>>,
560) -> Verdict<C, R> {
561    match consulted {
562        None => {
563            shared.log().warn(format!(
564                "H.323 gatekeeper: {name} from {peer} refused decision=fail_closed_in_flight_cap ({MAX_IN_FLIGHT} requests already with the model)"
565            ));
566            Verdict::Refuse("resourceUnavailable")
567        }
568        Some(Consulted::Failed { overloaded }) => Verdict::Refuse(if overloaded {
569            "resourceUnavailable"
570        } else {
571            "undefinedReason"
572        }),
573        Some(Consulted::Answered { commands, refused }) => {
574            for c in commands {
575                if let Some(v) = pick(c) {
576                    return match v {
577                        Ok(c) => Verdict::Confirm(c),
578                        Err(r) => Verdict::Reject(r),
579                    };
580                }
581            }
582            if refused.is_empty() {
583                shared.log().warn(format!(
584                    "H.323 gatekeeper: {name} from {peer} answered with no confirm or reject decision=model_silent; refusing"
585                ));
586                Verdict::Refuse("undefinedReason")
587            } else {
588                shared.log().error(format!(
589                    "H.323 gatekeeper: {name} from {peer}: no usable action decision=fail_closed_bad_action ({})",
590                    refused.join("; ")
591                ));
592                Verdict::Refuse("undefinedReason")
593            }
594        }
595    }
596}
597
598fn gk_identifier_ok(s: &str) -> bool {
599    !s.is_empty() && s.chars().count() <= 128 && !crate::utils::sanitize::has_controls(s)
600}
601
602async fn gatekeeper_request(
603    shared: &Shared,
604    id: ConnectionId,
605    peer: SocketAddr,
606    seq: u16,
607    body: &Value,
608) -> Option<(&'static str, Value)> {
609    let reply_to = body.get("rasAddress").and_then(h225::socket_addr);
610    let event = Event::new(
611        &act::GRQ_EVENT,
612        json!({
613            "from": peer.to_string(),
614            "ras_address": reply_to.map(|a| a.to_string()),
615            "endpoint": h225::endpoint_summary(body.get("endpointType")),
616            "aliases": h225::aliases_text(body.get("endpointAlias")),
617            "requested_gatekeeper": body.get("gatekeeperIdentifier").and_then(|g| g.as_str()).map(crate::utils::sanitize::strip_controls),
618            "this_gatekeeper": shared.config.gatekeeper_id,
619        }),
620    );
621    let consulted = consult(shared, id, (peer, seq), event).await;
622    let v = verdict(shared, "GRQ", peer, consulted, |c| match c {
623        GkCommand::ConfirmGatekeeper => Some(Ok(())),
624        GkCommand::RejectGatekeeper(r) => Some(Err(r)),
625        _ => None,
626    });
627    let base = json!({
628        "requestSeqNum": seq,
629        "protocolIdentifier": h225::PROTOCOL_ID,
630        "gatekeeperIdentifier": shared.config.gatekeeper_id,
631    });
632    match v {
633        Verdict::Confirm(()) => {
634            shared.log().info(format!(
635                "H.323 gatekeeper: GRQ from {peer} decision=model_answer; GCF"
636            ));
637            let mut b = base;
638            b["rasAddress"] = h225::transport(shared.ras_address_for(peer));
639            Some(("gatekeeperConfirm", b))
640        }
641        Verdict::Reject(reason) => {
642            shared.log().info(format!(
643                "H.323 gatekeeper: GRQ from {peer} decision=model_reject; GRJ {reason}"
644            ));
645            let mut b = base;
646            b["rejectReason"] = json!({ reason: null });
647            Some(("gatekeeperReject", b))
648        }
649        Verdict::Refuse(reason) => {
650            let mut b = base;
651            b["rejectReason"] = json!({ reason: null });
652            Some(("gatekeeperReject", b))
653        }
654    }
655}
656
657fn reject_registration(
658    seq: u16,
659    gk: &str,
660    reason: &str,
661    duplicates: Option<Vec<Value>>,
662) -> (&'static str, Value) {
663    let reason_value = match duplicates {
664        Some(d) if reason == "duplicateAlias" => json!({"duplicateAlias": d}),
665        _ if reason == "duplicateAlias" => json!({"duplicateAlias": []}),
666        _ => json!({ reason: null }),
667    };
668    (
669        "registrationReject",
670        json!({
671            "requestSeqNum": seq,
672            "protocolIdentifier": h225::PROTOCOL_ID,
673            "rejectReason": reason_value,
674            "gatekeeperIdentifier": gk,
675        }),
676    )
677}
678
679async fn registration_request(
680    shared: &Shared,
681    id: ConnectionId,
682    peer: SocketAddr,
683    seq: u16,
684    body: &Value,
685) -> Option<(&'static str, Value)> {
686    let gk = shared.config.gatekeeper_id.clone();
687    let call_signal: Vec<Value> = body
688        .get("callSignalAddress")
689        .and_then(|a| a.as_array())
690        .cloned()
691        .unwrap_or_default();
692    let ras: Vec<Value> = body
693        .get("rasAddress")
694        .and_then(|a| a.as_array())
695        .cloned()
696        .unwrap_or_default();
697    let keep_alive = body
698        .get("keepAlive")
699        .and_then(|k| k.as_bool())
700        .unwrap_or(false);
701    let requested_ttl = body.get("timeToLive").and_then(|t| t.as_u64());
702
703    if keep_alive {
704        let endpoint_id = body
705            .get("endpointIdentifier")
706            .and_then(|e| e.as_str())
707            .unwrap_or("");
708        let mut st = shared.state();
709        let Some(reg) = st.registrations.get_mut(endpoint_id) else {
710            drop(st);
711            shared.log().info(format!(
712                "H.323 gatekeeper: keep-alive RRQ from {peer} for unknown endpoint {endpoint_id:?} decision=protocol_reject; RRJ fullRegistrationRequired"
713            ));
714            return Some(reject_registration(
715                seq,
716                &gk,
717                "fullRegistrationRequired",
718                None,
719            ));
720        };
721        reg.expires = Instant::now() + Duration::from_secs(reg.ttl);
722        let answer = json!({
723            "requestSeqNum": seq,
724            "protocolIdentifier": h225::PROTOCOL_ID,
725            "callSignalAddress": [],
726            "gatekeeperIdentifier": gk,
727            "endpointIdentifier": reg.endpoint_id,
728            "timeToLive": reg.ttl,
729            "willRespondToIRR": false,
730            "maintainConnection": false,
731        });
732        shared.log().debug(format!(
733            "H.323 gatekeeper: keep-alive RRQ from {peer} refreshed {} for {}s",
734            reg.endpoint_id, reg.ttl
735        ));
736        return Some(("registrationConfirm", answer));
737    }
738
739    if call_signal.is_empty() {
740        shared.log().info(format!("H.323 gatekeeper: RRQ from {peer} with no call signalling address decision=protocol_reject"));
741        return Some(reject_registration(
742            seq,
743            &gk,
744            "invalidCallSignalAddress",
745            None,
746        ));
747    }
748    if ras.is_empty() {
749        shared.log().info(format!(
750            "H.323 gatekeeper: RRQ from {peer} with no RAS address decision=protocol_reject"
751        ));
752        return Some(reject_registration(seq, &gk, "invalidRASAddress", None));
753    }
754    let requested: Vec<Value> = body
755        .get("terminalAlias")
756        .and_then(|a| a.as_array())
757        .cloned()
758        .unwrap_or_default();
759    let (existing, conflicts, full) = {
760        let mut st = shared.state();
761        st.expire();
762        let existing = st.by_call_signal(&call_signal);
763        let conflicts: Vec<String> = requested
764            .iter()
765            .filter(|a| {
766                st.holder_of(a)
767                    .is_some_and(|r| Some(&r.endpoint_id) != existing.as_ref())
768            })
769            .map(h225::alias_text)
770            .collect();
771        let full = existing.is_none() && st.registrations.len() >= MAX_REGISTRATIONS;
772        (existing, conflicts, full)
773    };
774    if full {
775        shared.log().warn(format!(
776            "H.323 gatekeeper: RRQ from {peer} refused decision=fail_closed_registration_cap ({MAX_REGISTRATIONS} endpoints registered)"
777        ));
778        return Some(reject_registration(seq, &gk, "resourceUnavailable", None));
779    }
780    let event = Event::new(
781        &act::RRQ_EVENT,
782        json!({
783            "from": peer.to_string(),
784            "aliases": requested.iter().map(h225::alias_text).collect::<Vec<_>>(),
785            "call_signal_addresses": h225::addresses_text(Some(&Value::Array(call_signal.clone()))),
786            "ras_addresses": h225::addresses_text(Some(&Value::Array(ras.clone()))),
787            "endpoint": h225::endpoint_summary(body.get("terminalType")),
788            "vendor": h225::endpoint_summary(Some(&json!({"vendor": body.get("endpointVendor"), "terminal": {}}))),
789            "requested_time_to_live": requested_ttl,
790            "already_registered_as": existing,
791            "aliases_held_by_other_endpoints": conflicts,
792            "registered_endpoints": shared.state().registrations.len(),
793        }),
794    );
795    let consulted = consult(shared, id, (peer, seq), event).await;
796    let v = verdict(shared, "RRQ", peer, consulted, |c| match c {
797        GkCommand::ConfirmRegistration { aliases, ttl } => Some(Ok((aliases, ttl))),
798        GkCommand::RejectRegistration { reason, duplicates } => Some(Err((reason, duplicates))),
799        _ => None,
800    });
801    match v {
802        Verdict::Confirm((aliases, ttl)) => {
803            let aliases = aliases.unwrap_or(requested);
804            let ttl = ttl
805                .or(requested_ttl)
806                .unwrap_or(shared.config.default_ttl)
807                .clamp(1, MAX_TTL_SECS);
808            let mut st = shared.state();
809            let endpoint_id = match &existing {
810                Some(e) => e.clone(),
811                None => {
812                    st.next_endpoint += 1;
813                    format!("ep{}-{:04x}", st.next_endpoint, rand::random::<u16>())
814                }
815            };
816            let taken: Vec<Value> = aliases
817                .iter()
818                .filter(|a| {
819                    st.holder_of(a)
820                        .is_some_and(|r| r.endpoint_id != endpoint_id)
821                })
822                .cloned()
823                .collect();
824            if !taken.is_empty() {
825                drop(st);
826                shared.log().warn(format!(
827                    "H.323 gatekeeper: RRQ from {peer}: the model confirmed aliases another endpoint holds ({}) decision=fail_closed_bad_action; RRJ duplicateAlias",
828                    taken.iter().map(h225::alias_text).collect::<Vec<_>>().join(", ")
829                ));
830                return Some(reject_registration(seq, &gk, "duplicateAlias", Some(taken)));
831            }
832            if existing.is_none() && st.registrations.len() >= MAX_REGISTRATIONS {
833                drop(st);
834                return Some(reject_registration(seq, &gk, "resourceUnavailable", None));
835            }
836            st.registrations.insert(
837                endpoint_id.clone(),
838                Registration {
839                    endpoint_id: endpoint_id.clone(),
840                    aliases: aliases.clone(),
841                    call_signal,
842                    ras,
843                    ttl,
844                    expires: Instant::now() + Duration::from_secs(ttl),
845                    endpoint: h225::endpoint_summary(body.get("terminalType")),
846                },
847            );
848            drop(st);
849            shared.log().info(format!(
850                "H.323 gatekeeper: RRQ from {peer} decision=model_answer; RCF {endpoint_id} as [{}] for {ttl}s",
851                aliases.iter().map(h225::alias_text).collect::<Vec<_>>().join(", ")
852            ));
853            let mut answer = json!({
854                "requestSeqNum": seq,
855                "protocolIdentifier": h225::PROTOCOL_ID,
856                "callSignalAddress": [],
857                "gatekeeperIdentifier": gk,
858                "endpointIdentifier": endpoint_id,
859                "timeToLive": ttl,
860                "willRespondToIRR": false,
861                "maintainConnection": false,
862            });
863            if !aliases.is_empty() {
864                answer["terminalAlias"] = Value::Array(aliases);
865            }
866            Some(("registrationConfirm", answer))
867        }
868        Verdict::Reject((reason, duplicates)) => {
869            shared.log().info(format!(
870                "H.323 gatekeeper: RRQ from {peer} decision=model_reject; RRJ {reason}"
871            ));
872            Some(reject_registration(seq, &gk, &reason, duplicates))
873        }
874        Verdict::Refuse(reason) => Some(reject_registration(seq, &gk, reason, None)),
875    }
876}
877
878fn unregistration_request(
879    shared: &Shared,
880    seq: u16,
881    body: &Value,
882) -> Option<(&'static str, Value)> {
883    let call_signal: Vec<Value> = body
884        .get("callSignalAddress")
885        .and_then(|a| a.as_array())
886        .cloned()
887        .unwrap_or_default();
888    let mut st = shared.state();
889    let endpoint_id = body
890        .get("endpointIdentifier")
891        .and_then(|e| e.as_str())
892        .map(str::to_string)
893        .filter(|e| st.registrations.contains_key(e))
894        .or_else(|| st.by_call_signal(&call_signal));
895    match endpoint_id {
896        Some(e) => {
897            st.registrations.remove(&e);
898            st.calls.retain(|_, c| c.endpoint_id != e);
899            drop(st);
900            shared
901                .log()
902                .info(format!("H.323 gatekeeper: URQ: {e} unregistered; UCF"));
903            Some(("unregistrationConfirm", json!({"requestSeqNum": seq})))
904        }
905        None => Some((
906            "unregistrationReject",
907            json!({"requestSeqNum": seq, "rejectReason": {"notCurrentlyRegistered": null}}),
908        )),
909    }
910}
911
912fn disengage_request(shared: &Shared, seq: u16, body: &Value) -> Option<(&'static str, Value)> {
913    let endpoint_id = body
914        .get("endpointIdentifier")
915        .and_then(|e| e.as_str())
916        .unwrap_or("");
917    let mut st = shared.state();
918    if !st.registrations.contains_key(endpoint_id) {
919        return Some((
920            "disengageReject",
921            json!({"requestSeqNum": seq, "rejectReason": {"notRegistered": null}}),
922        ));
923    }
924    if let Some(guid) = body
925        .get("callIdentifier")
926        .and_then(|c| c.get("guid"))
927        .and_then(|g| g.as_str())
928    {
929        if let Some(call) = st.calls.remove(&format!("{endpoint_id}/{guid}")) {
930            shared.log().debug(format!(
931                "H.323 gatekeeper: call {guid} of {} released {} × 100 bit/s",
932                call.endpoint_id, call.bandwidth
933            ));
934        }
935    }
936    drop(st);
937    shared
938        .log()
939        .info(format!("H.323 gatekeeper: DRQ from {endpoint_id}; DCF"));
940    Some(("disengageConfirm", json!({"requestSeqNum": seq})))
941}
942
943/// Resolve the model's destination: a registered alias (to that endpoint's call signalling
944/// address), or a literal `ip:port`.
945/// A resolved destination: its call signalling address, its registered aliases, and its RAS
946/// addresses when it is a registered endpoint.
947type Resolved = (SocketAddr, Vec<Value>, Option<Vec<Value>>);
948
949fn resolve_destination(st: &State, destination: &str) -> Result<Resolved, String> {
950    if let Ok(addr) = destination.parse::<SocketAddr>() {
951        return Ok((addr, Vec::new(), None));
952    }
953    let alias = h225::alias(destination).map_err(|e| e.to_string())?;
954    let reg = st
955        .holder_of(&alias)
956        .ok_or_else(|| format!("no registered endpoint holds {destination}"))?;
957    let addr = reg
958        .call_signal
959        .iter()
960        .find_map(h225::socket_addr)
961        .ok_or_else(|| {
962            format!(
963                "{} registered no IP call signalling address",
964                reg.endpoint_id
965            )
966        })?;
967    Ok((addr, reg.aliases.clone(), Some(reg.ras.clone())))
968}
969
970async fn admission_request(
971    shared: &Shared,
972    id: ConnectionId,
973    peer: SocketAddr,
974    seq: u16,
975    body: &Value,
976) -> Option<(&'static str, Value)> {
977    let endpoint_id = body
978        .get("endpointIdentifier")
979        .and_then(|e| e.as_str())
980        .unwrap_or("")
981        .to_string();
982    let reject = |reason: &str| {
983        Some((
984            "admissionReject",
985            json!({"requestSeqNum": seq, "rejectReason": { reason: null }}),
986        ))
987    };
988    let answer_call = body
989        .get("answerCall")
990        .and_then(|a| a.as_bool())
991        .unwrap_or(false);
992    let destinations: Vec<Value> = body
993        .get("destinationInfo")
994        .and_then(|d| d.as_array())
995        .cloned()
996        .unwrap_or_default();
997    let (caller, candidates, calls_open) = {
998        let mut st = shared.state();
999        st.expire();
1000        let caller = st.registrations.get(&endpoint_id).cloned();
1001        let candidates: Vec<Value> = destinations
1002            .iter()
1003            .filter_map(|a| {
1004                st.holder_of(a).map(|r| {
1005                    json!({
1006                        "alias": h225::alias_text(a),
1007                        "endpoint_id": r.endpoint_id,
1008                        "call_signal_address": r.call_signal.iter().find_map(h225::socket_addr).map(|s| s.to_string()),
1009                        "endpoint": r.endpoint,
1010                    })
1011                })
1012            })
1013            .collect();
1014        (caller, candidates, st.calls.len())
1015    };
1016    let Some(caller) = caller else {
1017        shared.log().info(format!(
1018            "H.323 gatekeeper: ARQ from {peer} for unregistered endpoint {endpoint_id:?} decision=protocol_reject; ARJ callerNotRegistered"
1019        ));
1020        return reject("callerNotRegistered");
1021    };
1022    if calls_open >= MAX_CALLS {
1023        shared.log().warn(format!(
1024            "H.323 gatekeeper: ARQ from {peer} refused decision=fail_closed_call_cap ({MAX_CALLS} calls admitted)"
1025        ));
1026        return reject("resourceUnavailable");
1027    }
1028    let requested_bw = body.get("bandWidth").and_then(|b| b.as_u64()).unwrap_or(0);
1029    let call_id = body
1030        .get("callIdentifier")
1031        .and_then(|c| c.get("guid"))
1032        .and_then(|g| g.as_str())
1033        .unwrap_or("")
1034        .to_string();
1035    let event = Event::new(
1036        &act::ARQ_EVENT,
1037        json!({
1038            "from": peer.to_string(),
1039            "endpoint_id": endpoint_id,
1040            "caller_aliases": caller.aliases.iter().map(h225::alias_text).collect::<Vec<_>>(),
1041            "source_info": h225::aliases_text(body.get("srcInfo")),
1042            "destination_aliases": h225::aliases_text(body.get("destinationInfo")),
1043            "destination_address": body.get("destCallSignalAddress").map(h225::transport_text),
1044            "registered_destinations": candidates,
1045            "answer_call": answer_call,
1046            "bandwidth": requested_bw,
1047            "call_reference": body.get("callReferenceValue"),
1048            "call_id": call_id,
1049            "conference_id": body.get("conferenceID"),
1050        }),
1051    );
1052    let consulted = consult(shared, id, (peer, seq), event).await;
1053    let v = verdict(shared, "ARQ", peer, consulted, |c| match c {
1054        GkCommand::ConfirmAdmission {
1055            destination,
1056            bandwidth,
1057        } => Some(Ok((destination, bandwidth))),
1058        GkCommand::RejectAdmission(r) => Some(Err(r)),
1059        _ => None,
1060    });
1061    match v {
1062        Verdict::Confirm((destination, bandwidth)) => {
1063            let resolved = {
1064                let st = shared.state();
1065                match destination.as_deref() {
1066                    Some(d) => resolve_destination(&st, d),
1067                    None if answer_call => caller
1068                        .call_signal
1069                        .iter()
1070                        .find_map(h225::socket_addr)
1071                        .map(|a| (a, caller.aliases.clone(), None))
1072                        .ok_or_else(|| {
1073                            "the answering endpoint registered no IP address".to_string()
1074                        }),
1075                    None => destinations
1076                        .iter()
1077                        .find_map(|a| {
1078                            let r = st.holder_of(a)?;
1079                            let addr = r.call_signal.iter().find_map(h225::socket_addr)?;
1080                            Some((addr, r.aliases.clone(), None))
1081                        })
1082                        .or_else(|| {
1083                            body.get("destCallSignalAddress")
1084                                .and_then(h225::socket_addr)
1085                                .map(|a| (a, Vec::new(), None))
1086                        })
1087                        .ok_or_else(|| {
1088                            "no destination is registered and the ARQ named no address".to_string()
1089                        }),
1090                }
1091            };
1092            let (addr, dest_aliases, _) = match resolved {
1093                Ok(r) => r,
1094                Err(why) => {
1095                    shared.log().warn(format!(
1096                        "H.323 gatekeeper: ARQ from {peer}: cannot admit ({why}) decision=fail_closed_bad_action; ARJ calledPartyNotRegistered"
1097                    ));
1098                    return reject("calledPartyNotRegistered");
1099                }
1100            };
1101            let bw = bandwidth
1102                .unwrap_or(requested_bw)
1103                .min(shared.config.max_bandwidth);
1104            {
1105                let mut st = shared.state();
1106                if st.calls.len() < MAX_CALLS && !call_id.is_empty() {
1107                    st.calls.insert(
1108                        format!("{endpoint_id}/{call_id}"),
1109                        AdmittedCall {
1110                            endpoint_id: endpoint_id.clone(),
1111                            bandwidth: bw,
1112                        },
1113                    );
1114                }
1115            }
1116            shared.log().info(format!(
1117                "H.323 gatekeeper: ARQ from {endpoint_id} decision=model_answer; ACF to {addr}, {bw} × 100 bit/s"
1118            ));
1119            let mut answer = json!({
1120                "requestSeqNum": seq,
1121                "bandWidth": bw,
1122                "callModel": {"direct": null},
1123                "destCallSignalAddress": h225::transport(addr),
1124                "willRespondToIRR": false,
1125                "uuiesRequested": {
1126                    "setup": false, "callProceeding": false, "connect": false, "alerting": false,
1127                    "information": false, "releaseComplete": false, "facility": false,
1128                    "progress": false, "empty": false,
1129                },
1130            });
1131            if !dest_aliases.is_empty() {
1132                answer["destinationInfo"] = Value::Array(dest_aliases);
1133            }
1134            Some(("admissionConfirm", answer))
1135        }
1136        Verdict::Reject(reason) => {
1137            shared.log().info(format!(
1138                "H.323 gatekeeper: ARQ from {endpoint_id} decision=model_reject; ARJ {reason}"
1139            ));
1140            reject(&reason)
1141        }
1142        Verdict::Refuse(reason) => reject(reason),
1143    }
1144}
1145
1146async fn location_request(
1147    shared: &Shared,
1148    id: ConnectionId,
1149    peer: SocketAddr,
1150    seq: u16,
1151    body: &Value,
1152) -> Option<(&'static str, Value)> {
1153    let reject = |reason: &str| {
1154        Some((
1155            "locationReject",
1156            json!({"requestSeqNum": seq, "rejectReason": { reason: null }}),
1157        ))
1158    };
1159    let destinations: Vec<Value> = body
1160        .get("destinationInfo")
1161        .and_then(|d| d.as_array())
1162        .cloned()
1163        .unwrap_or_default();
1164    let candidates: Vec<Value> = {
1165        let st = shared.state();
1166        destinations
1167            .iter()
1168            .filter_map(|a| {
1169                st.holder_of(a).map(|r| json!({
1170                    "alias": h225::alias_text(a),
1171                    "endpoint_id": r.endpoint_id,
1172                    "call_signal_address": r.call_signal.iter().find_map(h225::socket_addr).map(|s| s.to_string()),
1173                }))
1174            })
1175            .collect()
1176    };
1177    let event = Event::new(
1178        &act::LRQ_EVENT,
1179        json!({
1180            "from": peer.to_string(),
1181            "destination_aliases": h225::aliases_text(body.get("destinationInfo")),
1182            "source_info": h225::aliases_text(body.get("sourceInfo")),
1183            "reply_address": body.get("replyAddress").map(h225::transport_text),
1184            "registered_destinations": candidates,
1185        }),
1186    );
1187    let consulted = consult(shared, id, (peer, seq), event).await;
1188    let v = verdict(shared, "LRQ", peer, consulted, |c| match c {
1189        GkCommand::ConfirmLocation { destination } => Some(Ok(destination)),
1190        GkCommand::RejectLocation(r) => Some(Err(r)),
1191        _ => None,
1192    });
1193    match v {
1194        Verdict::Confirm(destination) => {
1195            let resolved = {
1196                let st = shared.state();
1197                match destination.as_deref() {
1198                    Some(d) => resolve_destination(&st, d),
1199                    None => destinations
1200                        .iter()
1201                        .find_map(|a| {
1202                            let r = st.holder_of(a)?;
1203                            let addr = r.call_signal.iter().find_map(h225::socket_addr)?;
1204                            Some((addr, r.aliases.clone(), Some(r.ras.clone())))
1205                        })
1206                        .ok_or_else(|| "no destination alias is registered".to_string()),
1207                }
1208            };
1209            match resolved {
1210                Ok((addr, aliases, ras)) => {
1211                    let ras = ras
1212                        .and_then(|r| r.into_iter().next())
1213                        .unwrap_or_else(|| h225::transport(shared.ras_address_for(peer)));
1214                    shared.log().info(format!(
1215                        "H.323 gatekeeper: LRQ from {peer} decision=model_answer; LCF {addr}"
1216                    ));
1217                    let mut answer = json!({
1218                        "requestSeqNum": seq,
1219                        "callSignalAddress": h225::transport(addr),
1220                        "rasAddress": ras,
1221                    });
1222                    if !aliases.is_empty() {
1223                        answer["destinationInfo"] = Value::Array(aliases);
1224                    }
1225                    Some(("locationConfirm", answer))
1226                }
1227                Err(why) => {
1228                    shared.log().warn(format!(
1229                        "H.323 gatekeeper: LRQ from {peer}: cannot locate ({why}) decision=fail_closed_bad_action; LRJ notRegistered"
1230                    ));
1231                    reject("notRegistered")
1232                }
1233            }
1234        }
1235        Verdict::Reject(reason) => {
1236            shared.log().info(format!(
1237                "H.323 gatekeeper: LRQ from {peer} decision=model_reject; LRJ {reason}"
1238            ));
1239            reject(&reason)
1240        }
1241        Verdict::Refuse(reason) => {
1242            // LRJ has resourceUnavailable only as an extension; undefinedReason is root.
1243            reject(if reason == "resourceUnavailable" {
1244                "resourceUnavailable"
1245            } else {
1246                "undefinedReason"
1247            })
1248        }
1249    }
1250}
1251
1252pub fn validate_gatekeeper_id(s: &str) -> Result<()> {
1253    anyhow::ensure!(
1254        gk_identifier_ok(s),
1255        "gatekeeper_id is 1 to 128 characters with no control characters"
1256    );
1257    Ok(())
1258}