1use super::call::{self, cause, RELEASE_REASONS};
12use super::gatekeeper::{
13 self, ADMISSION_REASONS, GATEKEEPER_REASONS, LOCATION_REASONS, REGISTRATION_REASONS,
14};
15use super::{endpoint, h225, h245};
16use crate::llm::actions::{
17 protocol_trait::{ActionResult, Protocol, Server},
18 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
19};
20use crate::protocol::log_template::LogTemplate;
21use crate::protocol::EventType;
22use crate::state::app_state::AppState;
23use anyhow::{anyhow, bail, Context, Result};
24use serde_json::{json, Value};
25use std::net::SocketAddr;
26use std::sync::LazyLock;
27
28pub const DEFAULT_GATEKEEPER_ID: &str = "NetGet";
30pub const DEFAULT_MAX_BANDWIDTH: u64 = 100_000;
33
34pub const CONFIRM_GATEKEEPER: &str = "h323_confirm_gatekeeper";
35pub const REJECT_GATEKEEPER: &str = "h323_reject_gatekeeper";
36pub const CONFIRM_REGISTRATION: &str = "h323_confirm_registration";
37pub const REJECT_REGISTRATION: &str = "h323_reject_registration";
38pub const CONFIRM_ADMISSION: &str = "h323_confirm_admission";
39pub const REJECT_ADMISSION: &str = "h323_reject_admission";
40pub const CONFIRM_LOCATION: &str = "h323_confirm_location";
41pub const REJECT_LOCATION: &str = "h323_reject_location";
42
43pub const CALL_PROCEEDING: &str = "h323_call_proceeding";
44pub const ALERTING: &str = "h323_alerting";
45pub const CONNECT: &str = "h323_connect";
46pub const RELEASE_COMPLETE: &str = "h323_release_complete";
47pub const FORWARD_CALL: &str = "h323_forward_call";
48
49pub fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
50 Parameter {
51 name: name.into(),
52 type_hint: type_hint.into(),
53 description: description.into(),
54 required,
55 }
56}
57
58fn action(
59 name: &str,
60 description: &str,
61 parameters: Vec<Parameter>,
62 example: Value,
63) -> ActionDefinition {
64 ActionDefinition {
65 name: name.into(),
66 description: description.into(),
67 parameters,
68 example,
69 log_template: Some(
70 LogTemplate::new()
71 .with_info(format!("H.323 {name}"))
72 .with_debug(format!("H.323 {name}: {{json(.)}}")),
73 ),
74 }
75}
76
77fn reason_of(action: &Value, allowed: &[&str], default: &str) -> Result<String> {
78 let r = action
79 .get("reason")
80 .and_then(|r| r.as_str())
81 .unwrap_or(default)
82 .to_string();
83 if !allowed.contains(&r.as_str()) {
84 bail!("reason {r:?} is not one of {allowed:?}");
85 }
86 Ok(r)
87}
88
89#[derive(Debug, Clone)]
95pub enum GkCommand {
96 ConfirmGatekeeper,
97 RejectGatekeeper(String),
98 ConfirmRegistration {
99 aliases: Option<Vec<Value>>,
100 ttl: Option<u64>,
101 },
102 RejectRegistration {
103 reason: String,
104 duplicates: Option<Vec<Value>>,
105 },
106 ConfirmAdmission {
107 destination: Option<String>,
108 bandwidth: Option<u64>,
109 },
110 RejectAdmission(String),
111 ConfirmLocation {
112 destination: Option<String>,
113 },
114 RejectLocation(String),
115}
116
117fn destination_of(a: &Value) -> Result<Option<String>> {
118 match a.get("destination") {
119 None | Some(Value::Null) => Ok(None),
120 Some(Value::String(s)) => {
121 if s.parse::<SocketAddr>().is_err() {
122 h225::alias(s)?;
123 }
124 Ok(Some(s.clone()))
125 }
126 Some(other) => bail!("destination {other} is not a string"),
127 }
128}
129
130pub fn parse_gk_command(a: &Value) -> Result<GkCommand> {
131 let kind = a
132 .get("type")
133 .and_then(|t| t.as_str())
134 .ok_or_else(|| anyhow!("missing 'type'"))?;
135 Ok(match kind {
136 CONFIRM_GATEKEEPER => GkCommand::ConfirmGatekeeper,
137 REJECT_GATEKEEPER => {
138 GkCommand::RejectGatekeeper(reason_of(a, GATEKEEPER_REASONS, "terminalExcluded")?)
139 }
140 CONFIRM_REGISTRATION => {
141 let aliases = match a.get("aliases") {
142 None | Some(Value::Null) => None,
143 Some(v) => Some(h225::aliases(v)?),
144 };
145 let ttl = match a.get("time_to_live") {
146 None | Some(Value::Null) => None,
147 Some(t) => Some(
148 t.as_u64()
149 .filter(|t| (1..=gatekeeper::MAX_TTL_SECS).contains(t))
150 .ok_or_else(|| {
151 anyhow!("time_to_live is 1 to {} seconds", gatekeeper::MAX_TTL_SECS)
152 })?,
153 ),
154 };
155 GkCommand::ConfirmRegistration { aliases, ttl }
156 }
157 REJECT_REGISTRATION => {
158 let reason = reason_of(a, REGISTRATION_REASONS, "undefinedReason")?;
159 let duplicates = match a.get("aliases") {
160 Some(v) if reason == "duplicateAlias" => Some(h225::aliases(v)?),
161 _ => None,
162 };
163 GkCommand::RejectRegistration { reason, duplicates }
164 }
165 CONFIRM_ADMISSION => GkCommand::ConfirmAdmission {
166 destination: destination_of(a)?,
167 bandwidth: match a.get("bandwidth") {
168 None | Some(Value::Null) => None,
169 Some(b) => Some(
170 b.as_u64()
171 .filter(|b| *b <= u32::MAX as u64)
172 .ok_or_else(|| anyhow!("bandwidth is a number of 100 bit/s units"))?,
173 ),
174 },
175 },
176 REJECT_ADMISSION => {
177 GkCommand::RejectAdmission(reason_of(a, ADMISSION_REASONS, "requestDenied")?)
178 }
179 CONFIRM_LOCATION => GkCommand::ConfirmLocation {
180 destination: destination_of(a)?,
181 },
182 REJECT_LOCATION => {
183 GkCommand::RejectLocation(reason_of(a, LOCATION_REASONS, "requestDenied")?)
184 }
185 other => bail!("{other} is not an H.323 gatekeeper action"),
186 })
187}
188
189fn confirm_gatekeeper_action() -> ActionDefinition {
190 action(
191 CONFIRM_GATEKEEPER,
192 "Answer a GRQ with GCF: this gatekeeper will serve the endpoint, which normally \
193 registers next.",
194 vec![],
195 json!({"type": CONFIRM_GATEKEEPER}),
196 )
197}
198
199fn reject_gatekeeper_action() -> ActionDefinition {
200 action(
201 REJECT_GATEKEEPER,
202 "Answer a GRQ with GRJ: this gatekeeper will not serve the endpoint.",
203 vec![p(
204 "reason",
205 "string",
206 &format!("One of {}", GATEKEEPER_REASONS.join(", ")),
207 false,
208 )],
209 json!({"type": REJECT_GATEKEEPER, "reason": "terminalExcluded"}),
210 )
211}
212
213fn confirm_registration_action() -> ActionDefinition {
214 action(
215 CONFIRM_REGISTRATION,
216 "Answer an RRQ with RCF: register the endpoint. NetGet assigns its endpoint identifier \
217 and remembers the registration (aliases → call signalling address) until its \
218 time-to-live runs out; ARQ and LRQ then find it by alias.",
219 vec![
220 p(
221 "aliases",
222 "array",
223 "The aliases it is registered under, as \"kind:value\" (h323-ID:alice, \
224 dialledDigits:1001) or bare values; default the ones it asked for. An alias \
225 another endpoint holds is refused.",
226 false,
227 ),
228 p(
229 "time_to_live",
230 "number",
231 "Seconds the registration lasts before the endpoint must refresh it \
232 (default: what it asked for, else the configured default)",
233 false,
234 ),
235 ],
236 json!({"type": CONFIRM_REGISTRATION, "aliases": ["h323-ID:alice", "dialledDigits:1001"], "time_to_live": 300}),
237 )
238}
239
240fn reject_registration_action() -> ActionDefinition {
241 action(
242 REJECT_REGISTRATION,
243 "Answer an RRQ with RRJ: refuse to register the endpoint.",
244 vec![
245 p(
246 "reason",
247 "string",
248 &format!("One of {}", REGISTRATION_REASONS.join(", ")),
249 false,
250 ),
251 p(
252 "aliases",
253 "array",
254 "With reason duplicateAlias: the aliases already taken",
255 false,
256 ),
257 ],
258 json!({"type": REJECT_REGISTRATION, "reason": "securityDenial"}),
259 )
260}
261
262fn confirm_admission_action() -> ActionDefinition {
263 action(
264 CONFIRM_ADMISSION,
265 "Answer an ARQ with ACF: admit the call and tell the caller where to send its Setup. \
266 The destination is an alias of a registered endpoint (its registered call signalling \
267 address is used) or an explicit ip:port. Default: for answer_call the endpoint \
268 itself, otherwise the first registered destination alias, else the address the ARQ \
269 named.",
270 vec![
271 p(
272 "destination",
273 "string",
274 "A registered alias (h323-ID:bob, 1002) or ip:port",
275 false,
276 ),
277 p(
278 "bandwidth",
279 "number",
280 "Bandwidth granted in units of 100 bit/s (default: what was asked, capped)",
281 false,
282 ),
283 ],
284 json!({"type": CONFIRM_ADMISSION, "destination": "h323-ID:bob"}),
285 )
286}
287
288fn reject_admission_action() -> ActionDefinition {
289 action(
290 REJECT_ADMISSION,
291 "Answer an ARQ with ARJ: the call may not proceed.",
292 vec![p(
293 "reason",
294 "string",
295 &format!("One of {}", ADMISSION_REASONS.join(", ")),
296 false,
297 )],
298 json!({"type": REJECT_ADMISSION, "reason": "calledPartyNotRegistered"}),
299 )
300}
301
302fn confirm_location_action() -> ActionDefinition {
303 action(
304 CONFIRM_LOCATION,
305 "Answer an LRQ with LCF: say where the alias can be called. Destination as for \
306 h323_confirm_admission; default the first registered destination alias.",
307 vec![p(
308 "destination",
309 "string",
310 "A registered alias or ip:port",
311 false,
312 )],
313 json!({"type": CONFIRM_LOCATION, "destination": "h323-ID:bob"}),
314 )
315}
316
317fn reject_location_action() -> ActionDefinition {
318 action(
319 REJECT_LOCATION,
320 "Answer an LRQ with LRJ: this gatekeeper cannot locate the alias.",
321 vec![p(
322 "reason",
323 "string",
324 &format!("One of {}", LOCATION_REASONS.join(", ")),
325 false,
326 )],
327 json!({"type": REJECT_LOCATION, "reason": "notRegistered"}),
328 )
329}
330
331pub fn gatekeeper_actions() -> Vec<ActionDefinition> {
332 vec![
333 confirm_gatekeeper_action(),
334 reject_gatekeeper_action(),
335 confirm_registration_action(),
336 reject_registration_action(),
337 confirm_admission_action(),
338 reject_admission_action(),
339 confirm_location_action(),
340 reject_location_action(),
341 ]
342}
343
344pub static GRQ_EVENT: LazyLock<EventType> = LazyLock::new(|| {
345 EventType::new(
346 "h323_gatekeeper_request",
347 "An endpoint is looking for a gatekeeper (GRQ). Accept with h323_confirm_gatekeeper or \
348 refuse with h323_reject_gatekeeper; anything else is answered GRJ",
349 json!({"type": CONFIRM_GATEKEEPER}),
350 )
351 .with_parameters(vec![
352 p("from", "string", "Where the GRQ came from", true),
353 p(
354 "ras_address",
355 "string",
356 "The RAS address it asks to be answered on",
357 false,
358 ),
359 p(
360 "endpoint",
361 "object",
362 "What it is (terminal, gateway, mcu) and its product and version",
363 true,
364 ),
365 p("aliases", "array", "Aliases it named", true),
366 p(
367 "requested_gatekeeper",
368 "string",
369 "The gatekeeper identifier it asked for, if any",
370 false,
371 ),
372 p(
373 "this_gatekeeper",
374 "string",
375 "This gatekeeper's identifier",
376 true,
377 ),
378 ])
379 .with_actions(vec![
380 confirm_gatekeeper_action(),
381 reject_gatekeeper_action(),
382 ])
383 .with_alternative_example(json!({"type": REJECT_GATEKEEPER, "reason": "terminalExcluded"}))
384 .with_log_template(
385 LogTemplate::new()
386 .with_info("H.323 GRQ from {from}")
387 .with_debug("H.323 GRQ: {json(.)}"),
388 )
389});
390
391pub static RRQ_EVENT: LazyLock<EventType> = LazyLock::new(|| {
392 EventType::new(
393 "h323_registration_request",
394 "An endpoint asks to register (full RRQ). Register it with h323_confirm_registration \
395 (choosing its aliases and time-to-live) or refuse with h323_reject_registration; \
396 anything else is answered RRJ",
397 json!({"type": CONFIRM_REGISTRATION}),
398 )
399 .with_parameters(vec![
400 p("from", "string", "Where the RRQ came from", true),
401 p(
402 "aliases",
403 "array",
404 "The aliases it asks for, as kind:value",
405 true,
406 ),
407 p(
408 "call_signal_addresses",
409 "array",
410 "Where it takes calls (ip:port)",
411 true,
412 ),
413 p("ras_addresses", "array", "Its RAS addresses", true),
414 p(
415 "endpoint",
416 "object",
417 "What it is (terminal, gateway, mcu) and its product and version",
418 true,
419 ),
420 p(
421 "vendor",
422 "object",
423 "endpointVendor: product and version",
424 true,
425 ),
426 p(
427 "requested_time_to_live",
428 "number",
429 "The time-to-live it asked for, if any",
430 false,
431 ),
432 p(
433 "already_registered_as",
434 "string",
435 "Its endpoint identifier if it is re-registering",
436 false,
437 ),
438 p(
439 "aliases_held_by_other_endpoints",
440 "array",
441 "Requested aliases another endpoint already holds",
442 true,
443 ),
444 p(
445 "registered_endpoints",
446 "number",
447 "How many endpoints are registered now",
448 true,
449 ),
450 ])
451 .with_actions(vec![
452 confirm_registration_action(),
453 reject_registration_action(),
454 ])
455 .with_alternative_example(json!({"type": REJECT_REGISTRATION, "reason": "securityDenial"}))
456 .with_log_template(
457 LogTemplate::new()
458 .with_info("H.323 RRQ from {from} for {aliases}")
459 .with_debug("H.323 RRQ: {json(.)}"),
460 )
461});
462
463pub static ARQ_EVENT: LazyLock<EventType> = LazyLock::new(|| {
464 EventType::new(
465 "h323_admission_request",
466 "A registered endpoint asks to place (or, with answer_call, to answer) a call (ARQ). \
467 Admit it with h323_confirm_admission, choosing the destination, or refuse with \
468 h323_reject_admission; anything else is answered ARJ",
469 json!({"type": CONFIRM_ADMISSION}),
470 )
471 .with_parameters(vec![
472 p("from", "string", "Where the ARQ came from", true),
473 p(
474 "endpoint_id",
475 "string",
476 "The caller's endpoint identifier",
477 true,
478 ),
479 p(
480 "caller_aliases",
481 "array",
482 "The aliases it is registered under",
483 true,
484 ),
485 p("source_info", "array", "srcInfo aliases in the ARQ", true),
486 p("destination_aliases", "array", "Who it wants to call", true),
487 p(
488 "destination_address",
489 "string",
490 "An address it named directly, if any",
491 false,
492 ),
493 p(
494 "registered_destinations",
495 "array",
496 "Destination aliases that are registered here, with their endpoint and address",
497 true,
498 ),
499 p(
500 "answer_call",
501 "boolean",
502 "True when the endpoint asks to answer an incoming call",
503 true,
504 ),
505 p(
506 "bandwidth",
507 "number",
508 "Bandwidth asked for, in 100 bit/s units",
509 true,
510 ),
511 p("call_reference", "number", "The call reference value", true),
512 p("call_id", "string", "The call identifier (GUID, hex)", true),
513 p(
514 "conference_id",
515 "string",
516 "The conference identifier (hex)",
517 true,
518 ),
519 ])
520 .with_actions(vec![confirm_admission_action(), reject_admission_action()])
521 .with_alternative_example(
522 json!({"type": REJECT_ADMISSION, "reason": "calledPartyNotRegistered"}),
523 )
524 .with_log_template(
525 LogTemplate::new()
526 .with_info("H.323 ARQ from {endpoint_id} to {destination_aliases}")
527 .with_debug("H.323 ARQ: {json(.)}"),
528 )
529});
530
531pub static LRQ_EVENT: LazyLock<EventType> = LazyLock::new(|| {
532 EventType::new(
533 "h323_location_request",
534 "Another gatekeeper or an endpoint asks where an alias can be called (LRQ). Answer with \
535 h323_confirm_location or h323_reject_location; anything else is answered LRJ",
536 json!({"type": CONFIRM_LOCATION}),
537 )
538 .with_parameters(vec![
539 p("from", "string", "Where the LRQ came from", true),
540 p(
541 "destination_aliases",
542 "array",
543 "The aliases looked for",
544 true,
545 ),
546 p("source_info", "array", "Who is asking, as aliases", true),
547 p(
548 "reply_address",
549 "string",
550 "Where the answer is wanted",
551 false,
552 ),
553 p(
554 "registered_destinations",
555 "array",
556 "Which of the aliases are registered here",
557 true,
558 ),
559 ])
560 .with_actions(vec![confirm_location_action(), reject_location_action()])
561 .with_alternative_example(json!({"type": REJECT_LOCATION, "reason": "notRegistered"}))
562 .with_log_template(
563 LogTemplate::new().with_info("H.323 LRQ from {from} for {destination_aliases}"),
564 )
565});
566
567fn gatekeeper_event_types() -> Vec<EventType> {
568 vec![
569 GRQ_EVENT.clone(),
570 RRQ_EVENT.clone(),
571 ARQ_EVENT.clone(),
572 LRQ_EVENT.clone(),
573 ]
574}
575
576#[derive(Clone, Default)]
577pub struct H323GatekeeperProtocol;
578
579impl H323GatekeeperProtocol {
580 pub fn new() -> Self {
581 Self
582 }
583}
584
585impl Protocol for H323GatekeeperProtocol {
586 fn protocol_name(&self) -> &'static str {
587 "H323-Gatekeeper"
588 }
589 fn stack_name(&self) -> &'static str {
590 "ETH>IP>UDP>H.225.0 RAS"
591 }
592 fn description(&self) -> &'static str {
593 "H.323 gatekeeper (H.225.0 RAS) whose discovery, registration, admission and location answers the LLM decides"
594 }
595 fn example_prompt(&self) -> &'static str {
596 "Start an H.323 gatekeeper on port 1719; register anyone whose alias starts with 10, and admit calls only between registered endpoints"
597 }
598 fn keywords(&self) -> Vec<&'static str> {
599 vec![
600 "h323 gatekeeper",
601 "h.323 gatekeeper",
602 "gatekeeper",
603 "h.225 ras",
604 "h225 ras",
605 "ras",
606 "rrq",
607 "arq",
608 ]
609 }
610 fn group_name(&self) -> &'static str {
611 "VoIP & Media"
612 }
613
614 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
615 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
616 ProtocolMetadataV2::builder()
617 .state(DevelopmentState::Experimental)
618 .well_known_udp_port(1719)
619 .connectionless()
620 .answers_on_failure()
621 .max_inbound_bytes(gatekeeper::MAX_RAS_DATAGRAM)
622 .implementation(
623 "Hand-written ALIGNED PER (X.691) interpreted over schema tables generated from \
624 the ITU H.225.0 (12/2009), H.235.0 and H.245 ASN.1 modules \
625 (scripts/h323_asn1_schema.py): every RAS message decodes, and NetGet answers \
626 GRQ, RRQ (full and keep-alive), URQ, ARQ, DRQ, LRQ, BRQ, IRR (IACK), RAI and \
627 retransmissions (cached answer, or RIP while the model decides). \
628 Registrations and admitted calls are bounded runtime state with time-to-live",
629 )
630 .llm_control(
631 "GCF/GRJ, RCF (aliases, time-to-live)/RRJ, ACF (destination: a registered alias \
632 or ip:port, bandwidth)/ARJ and LCF/LRJ",
633 )
634 .e2e_testing(
635 "tests/server/h323: Asterisk's chan_ooh323 (ooh323c) and H323Plus's simph323 \
636 each register with NetGet's gatekeeper and are admitted to call NetGet's \
637 endpoint; a refused registration shows in the peer's own log; tshark's h225 \
638 dissector reads every RAS message both ways from a live capture",
639 )
640 .notes(
641 "Experimental: two independent H.323 implementations (ooh323c and H323Plus) \
642 complete registration and admission against it in tests that fail rather than \
643 skip, and its PER decoder has a fuzz target (fuzz/fuzz_targets/h323_per.rs, \
644 with a depth bomb in its corpus, run once for 500 000 inputs without a \
645 sanitizer), but nobody has reviewed it against H.225.0 clause by clause and its \
646 pair job had not run in CI when it landed. \
647 Direct call model only (ACF names the destination; the gatekeeper does not route \
648 signalling). No H.235 security: tokens are decoded past by length and never \
649 checked. No alternate gatekeepers, no inter-gatekeeper LRQ forwarding.",
650 )
651 .build()
652 }
653
654 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
655 vec![
656 ParameterDefinition {
657 name: "gatekeeper_id".into(),
658 type_hint: "string".into(),
659 description: "The gatekeeper identifier sent in GCF, RCF and rejects".into(),
660 required: false,
661 example: json!("NetGetGK"),
662 default: Some(json!(DEFAULT_GATEKEEPER_ID)),
663 },
664 ParameterDefinition {
665 name: "time_to_live".into(),
666 type_hint: "number".into(),
667 description: "Registration time-to-live in seconds when neither the endpoint nor \
668 the model chooses one"
669 .into(),
670 required: false,
671 example: json!(600),
672 default: Some(json!(gatekeeper::DEFAULT_TTL_SECS)),
673 },
674 ParameterDefinition {
675 name: "max_bandwidth".into(),
676 type_hint: "number".into(),
677 description: "The most bandwidth one call is granted, in units of 100 bit/s".into(),
678 required: false,
679 example: json!(1280),
680 default: Some(json!(DEFAULT_MAX_BANDWIDTH)),
681 },
682 ]
683 }
684
685 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
686 Vec::new()
687 }
688
689 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
690 gatekeeper_actions()
691 }
692
693 fn get_event_types(&self) -> Vec<EventType> {
694 gatekeeper_event_types()
695 }
696
697 fn get_startup_examples(&self) -> StartupExamples {
698 StartupExamples::new(
699 json!({
700 "type": "open_server",
701 "port": 1719,
702 "base_stack": "H323-Gatekeeper",
703 "startup_params": {"gatekeeper_id": "NetGetGK"},
704 "instruction": "Accept every GRQ. Register endpoints whose aliases are digits starting with 10; refuse others with securityDenial. Admit calls to registered aliases only."
705 }),
706 json!({
707 "type": "open_server",
708 "port": 1719,
709 "base_stack": "H323-Gatekeeper",
710 "event_handlers": [{
711 "event_pattern": "*",
712 "handler": {
713 "type": "script",
714 "language": "python",
715 "code": "import json,sys\ni=json.load(sys.stdin); t=i['event_type_id']; e=i['event']\na=[]\nif t=='h323_gatekeeper_request': a=[{'type':'h323_confirm_gatekeeper'}]\nelif t=='h323_registration_request': a=[{'type':'h323_confirm_registration'}]\nelif t=='h323_admission_request':\n a=[{'type':'h323_confirm_admission'}] if e.get('registered_destinations') or e.get('answer_call') else [{'type':'h323_reject_admission','reason':'calledPartyNotRegistered'}]\nelif t=='h323_location_request': a=[{'type':'h323_reject_location','reason':'notRegistered'}]\nprint(json.dumps({'actions':a}))"
716 }
717 }]
718 }),
719 json!({
720 "type": "open_server",
721 "port": 1719,
722 "base_stack": "H323-Gatekeeper",
723 "event_handlers": [
724 {"event_pattern": "h323_gatekeeper_request", "handler": {"type": "static", "actions": [{"type": CONFIRM_GATEKEEPER}]}},
725 {"event_pattern": "h323_registration_request", "handler": {"type": "static", "actions": [{"type": CONFIRM_REGISTRATION, "time_to_live": 300}]}},
726 {"event_pattern": "h323_admission_request", "handler": {"type": "static", "actions": [{"type": CONFIRM_ADMISSION}]}},
727 {"event_pattern": "*", "handler": {"type": "static", "actions": [{"type": REJECT_LOCATION, "reason": "notRegistered"}]}}
728 ]
729 }),
730 )
731 }
732}
733
734impl Server for H323GatekeeperProtocol {
735 fn spawn(
736 &self,
737 ctx: crate::protocol::SpawnContext,
738 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<SocketAddr>> + Send>> {
739 Box::pin(async move {
740 let params = ctx.startup_params.as_ref();
741 let gatekeeper_id = params
742 .map(|p| p.get_optional_string("gatekeeper_id"))
743 .transpose()?
744 .flatten()
745 .unwrap_or_else(|| DEFAULT_GATEKEEPER_ID.to_string());
746 gatekeeper::validate_gatekeeper_id(&gatekeeper_id)?;
747 let default_ttl = params
748 .map(|p| p.get_optional_u64("time_to_live"))
749 .transpose()?
750 .flatten()
751 .unwrap_or(gatekeeper::DEFAULT_TTL_SECS);
752 anyhow::ensure!(
753 (1..=gatekeeper::MAX_TTL_SECS).contains(&default_ttl),
754 "time_to_live is 1 to {} seconds",
755 gatekeeper::MAX_TTL_SECS
756 );
757 let max_bandwidth = params
758 .map(|p| p.get_optional_u64("max_bandwidth"))
759 .transpose()?
760 .flatten()
761 .unwrap_or(DEFAULT_MAX_BANDWIDTH)
762 .min(u32::MAX as u64);
763 gatekeeper::spawn(
764 ctx.legacy_listen_addr(),
765 ctx.llm_client,
766 ctx.state,
767 ctx.status_tx,
768 ctx.server_id,
769 gatekeeper::GatekeeperConfig {
770 gatekeeper_id,
771 default_ttl,
772 max_bandwidth,
773 },
774 )
775 .await
776 })
777 }
778
779 fn execute_action(&self, action: Value) -> Result<ActionResult> {
780 parse_gk_command(&action)?;
781 Ok(ActionResult::Custom {
782 name: action["type"].as_str().unwrap_or_default().to_string(),
783 data: action,
784 })
785 }
786}
787
788#[derive(Debug, Clone)]
794pub enum CallCommand {
795 Proceeding,
796 Alerting,
797 Connect {
798 codecs: Vec<h245::Codec>,
799 display: Option<String>,
800 },
801 Release {
802 cause: u8,
803 reason: Option<String>,
804 },
805 Forward {
806 address: Option<SocketAddr>,
807 aliases: Vec<Value>,
808 },
809}
810
811impl CallCommand {
812 pub fn name(&self) -> &'static str {
813 match self {
814 CallCommand::Proceeding => "CallProceeding",
815 CallCommand::Alerting => "Alerting",
816 CallCommand::Connect { .. } => "Connect",
817 CallCommand::Release { .. } => "ReleaseComplete",
818 CallCommand::Forward { .. } => "Facility (callForwarded)",
819 }
820 }
821}
822
823pub fn display_of(a: &Value) -> Result<Option<String>> {
825 match a.get("display") {
826 None | Some(Value::Null) => Ok(None),
827 Some(Value::String(s)) => {
828 if s.is_empty() || s.chars().count() > 80 || !s.is_ascii() {
829 bail!("display is 1 to 80 ASCII characters");
830 }
831 if crate::utils::sanitize::has_controls(s) {
832 bail!("display contains a control character");
833 }
834 Ok(Some(s.clone()))
835 }
836 Some(other) => bail!("display {other} is not a string"),
837 }
838}
839
840pub fn release_of(a: &Value) -> Result<(u8, Option<String>)> {
842 let c = match a.get("cause") {
843 None | Some(Value::Null) => cause::NORMAL_CLEARING,
844 Some(v) => v
845 .as_u64()
846 .filter(|c| (1..=127).contains(c))
847 .map(|c| c as u8)
848 .ok_or_else(|| anyhow!("cause is a Q.850 cause value, 1 to 127"))?,
849 };
850 let reason = match a.get("reason") {
851 None | Some(Value::Null) => None,
852 Some(Value::String(r)) => {
853 if !RELEASE_REASONS.contains(&r.as_str()) {
854 bail!("reason {r:?} is not one of {RELEASE_REASONS:?}");
855 }
856 Some(r.clone())
857 }
858 Some(other) => bail!("reason {other} is not a string"),
859 };
860 Ok((c, reason))
861}
862
863pub fn parse_call_command(a: &Value) -> Result<CallCommand> {
864 let kind = a
865 .get("type")
866 .and_then(|t| t.as_str())
867 .ok_or_else(|| anyhow!("missing 'type'"))?;
868 Ok(match kind {
869 CALL_PROCEEDING => CallCommand::Proceeding,
870 ALERTING => CallCommand::Alerting,
871 CONNECT => CallCommand::Connect {
872 codecs: match a.get("codec") {
873 None | Some(Value::Null) => Vec::new(),
874 v => call::codecs_from(v)?,
875 },
876 display: display_of(a)?,
877 },
878 RELEASE_COMPLETE => {
879 let (cause, reason) = release_of(a)?;
880 CallCommand::Release { cause, reason }
881 }
882 FORWARD_CALL => {
883 let to = a
884 .get("destination")
885 .and_then(|d| d.as_str())
886 .context("destination (an alias or ip:port) is required")?;
887 match to.parse::<SocketAddr>() {
888 Ok(addr) => CallCommand::Forward {
889 address: Some(addr),
890 aliases: Vec::new(),
891 },
892 Err(_) => CallCommand::Forward {
893 address: None,
894 aliases: vec![h225::alias(to)?],
895 },
896 }
897 }
898 other => bail!("{other} is not an H.323 call action"),
899 })
900}
901
902fn proceeding_action() -> ActionDefinition {
903 action(
904 CALL_PROCEEDING,
905 "Send CallProceeding: the call is being handled (stops the caller's Setup timer).",
906 vec![],
907 json!({"type": CALL_PROCEEDING}),
908 )
909}
910
911fn alerting_action() -> ActionDefinition {
912 action(
913 ALERTING,
914 "Send Alerting: the called party is ringing. A call left ringing is released after \
915 two minutes unless h323_connect follows (later, from the dashboard or MCP).",
916 vec![],
917 json!({"type": ALERTING}),
918 )
919}
920
921fn connect_action() -> ActionDefinition {
922 action(
923 CONNECT,
924 "Answer the call (Connect). NetGet accepts the caller's fastStart offer in the first \
925 codec both sides have and runs tunnelled H.245 (capability exchange, master-slave \
926 determination, logical channels) itself.",
927 vec![
928 p(
929 "codec",
930 "string",
931 "Preferred codec, g711u or g711a, or a list in order (default: as configured)",
932 false,
933 ),
934 p(
935 "display",
936 "string",
937 "Display name sent with the Connect (ASCII, up to 80 characters)",
938 false,
939 ),
940 ],
941 json!({"type": CONNECT, "codec": "g711u"}),
942 )
943}
944
945pub fn release_action() -> ActionDefinition {
946 action(
947 RELEASE_COMPLETE,
948 "End or refuse the call with Release Complete.",
949 vec![
950 p(
951 "cause",
952 "number",
953 "Q.850 cause: 16 normal clearing (default), 17 user busy, 19 no answer, 21 call \
954 rejected, 1 unallocated number, 31 normal unspecified",
955 false,
956 ),
957 p(
958 "reason",
959 "string",
960 &format!("H.225.0 reason, one of {}", RELEASE_REASONS.join(", ")),
961 false,
962 ),
963 ],
964 json!({"type": RELEASE_COMPLETE, "cause": 17, "reason": "inConf"}),
965 )
966}
967
968fn forward_action() -> ActionDefinition {
969 action(
970 FORWARD_CALL,
971 "Deflect the call elsewhere: Facility (callForwarded) naming the new destination, then \
972 Release Complete. The caller places a new call there.",
973 vec![p(
974 "destination",
975 "string",
976 "An alias (h323-ID:bob, 1002) or ip:port to call instead",
977 true,
978 )],
979 json!({"type": FORWARD_CALL, "destination": "h323-ID:voicemail"}),
980 )
981}
982
983pub fn endpoint_actions() -> Vec<ActionDefinition> {
984 vec![
985 proceeding_action(),
986 alerting_action(),
987 connect_action(),
988 release_action(),
989 forward_action(),
990 ]
991}
992
993pub static SETUP_EVENT: LazyLock<EventType> = LazyLock::new(|| {
994 EventType::new(
995 "h323_setup",
996 "A caller sent Setup. Answer with h323_connect (h323_call_proceeding and h323_alerting \
997 may come first), refuse with h323_release_complete, or deflect with h323_forward_call. \
998 An answer with none of these releases the call",
999 json!({"type": CONNECT}),
1000 )
1001 .with_parameters(vec![
1002 p("remote", "string", "The caller's address", true),
1003 p("crv", "number", "The Q.931 call reference value", true),
1004 p(
1005 "call_id",
1006 "string",
1007 "The H.225.0 call identifier (GUID, hex)",
1008 true,
1009 ),
1010 p(
1011 "conference_id",
1012 "string",
1013 "The conference identifier (hex)",
1014 true,
1015 ),
1016 p(
1017 "calling_number",
1018 "string",
1019 "Calling party number IE, if present",
1020 false,
1021 ),
1022 p(
1023 "called_number",
1024 "string",
1025 "Called party number IE, if present",
1026 false,
1027 ),
1028 p("display", "string", "Display IE, if present", false),
1029 p(
1030 "source_aliases",
1031 "array",
1032 "The caller's aliases (kind:value)",
1033 true,
1034 ),
1035 p(
1036 "destination_aliases",
1037 "array",
1038 "Who it is calling (kind:value)",
1039 true,
1040 ),
1041 p(
1042 "source_endpoint",
1043 "object",
1044 "What the caller is (terminal, gateway, ...) and its product and version",
1045 true,
1046 ),
1047 p(
1048 "source_call_signal_address",
1049 "string",
1050 "The caller's signalling address, if given",
1051 false,
1052 ),
1053 p(
1054 "fast_start",
1055 "array",
1056 "fastStart proposals: direction and codec of each",
1057 true,
1058 ),
1059 p(
1060 "h245_tunnelling",
1061 "boolean",
1062 "Whether the caller tunnels H.245",
1063 true,
1064 ),
1065 p(
1066 "h245_address",
1067 "string",
1068 "A separate H.245 address the caller offered, if any",
1069 false,
1070 ),
1071 p(
1072 "endpoint_identifier",
1073 "string",
1074 "The caller's endpoint identifier at its gatekeeper, if given",
1075 false,
1076 ),
1077 ])
1078 .with_actions(endpoint_actions())
1079 .with_alternative_example(
1080 json!({"type": RELEASE_COMPLETE, "cause": 21, "reason": "destinationRejection"}),
1081 )
1082 .with_log_template(
1083 LogTemplate::new()
1084 .with_info("H.323 Setup from {remote}: {calling_number} → {called_number}")
1085 .with_debug("H.323 Setup: {json(.)}"),
1086 )
1087});
1088
1089pub static USER_INPUT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1090 EventType::new(
1091 "h323_user_input",
1092 "The caller sent user input (DTMF) over H.245 during the call. Hang up with \
1093 h323_release_complete, or do nothing",
1094 json!({"type": RELEASE_COMPLETE}),
1095 )
1096 .with_parameters(vec![
1097 p("input", "string", "The digit or text", true),
1098 p("call_id", "string", "The call identifier", true),
1099 p("state", "string", "The call's state", true),
1100 ])
1101 .with_actions(vec![release_action()])
1102 .with_log_template(LogTemplate::new().with_info("H.323 user input {input}"))
1103});
1104
1105#[derive(Clone, Default)]
1106pub struct H323EndpointProtocol;
1107
1108impl H323EndpointProtocol {
1109 pub fn new() -> Self {
1110 Self
1111 }
1112}
1113
1114pub fn codecs_parameter() -> ParameterDefinition {
1115 ParameterDefinition {
1116 name: "codecs".into(),
1117 type_hint: "array".into(),
1118 description: "Audio codecs NetGet negotiates, in order of preference: g711u, g711a".into(),
1119 required: false,
1120 example: json!(["g711a", "g711u"]),
1121 default: Some(json!(h245::ALL_CODECS
1122 .iter()
1123 .map(|c| c.name())
1124 .collect::<Vec<_>>())),
1125 }
1126}
1127
1128pub fn codecs_from_params(
1129 params: Option<&crate::protocol::StartupParams>,
1130) -> Result<Vec<h245::Codec>> {
1131 let v = params
1132 .map(|p| p.get_optional_array("codecs"))
1133 .transpose()?
1134 .flatten();
1135 call::codecs_from(v.map(|a| Value::Array(a.clone())).as_ref())
1136}
1137
1138impl Protocol for H323EndpointProtocol {
1139 fn protocol_name(&self) -> &'static str {
1140 "H323"
1141 }
1142 fn stack_name(&self) -> &'static str {
1143 "ETH>IP>TCP>TPKT>Q.931>H.225.0"
1144 }
1145 fn description(&self) -> &'static str {
1146 "H.323 endpoint (H.225.0 call signalling over Q.931) that answers, rejects or forwards calls as the LLM decides, with fastStart and tunnelled H.245 for G.711"
1147 }
1148 fn example_prompt(&self) -> &'static str {
1149 "Start an H.323 endpoint on port 1720; answer calls to 1001 and reject everything else as busy"
1150 }
1151 fn keywords(&self) -> Vec<&'static str> {
1152 vec![
1153 "h323",
1154 "h.323",
1155 "h.225",
1156 "h225",
1157 "q.931",
1158 "q931",
1159 "h.245",
1160 "video conferencing",
1161 "netmeeting",
1162 ]
1163 }
1164 fn group_name(&self) -> &'static str {
1165 "VoIP & Media"
1166 }
1167
1168 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
1169 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
1170 ProtocolMetadataV2::builder()
1171 .state(DevelopmentState::Experimental)
1172 .well_known_port(1720)
1173 .answers_on_failure()
1174 .max_inbound_bytes(super::q931::MAX_TPKT)
1175 .implementation(
1176 "TPKT and the H.225.0 profile of Q.931 by hand, H.225.0 UUIEs and H.245 in \
1177 hand-written ALIGNED PER over schema tables generated from the ITU ASN.1 \
1178 (scripts/h323_asn1_schema.py). The called side of one call per connection: \
1179 Setup → CallProceeding/Alerting/Connect or Release Complete or a Facility \
1180 forward; fastStart answered in G.711; tunnelled H.245 (TerminalCapabilitySet, \
1181 MasterSlaveDetermination, OpenLogicalChannel, CloseLogicalChannel, \
1182 RoundTripDelay, EndSession, user input); RTP/RTCP sockets bound and counted, \
1183 no media sent",
1184 )
1185 .llm_control(
1186 "Whether each call is answered (and in which codec), left ringing, rejected with \
1187 which Q.850 cause and H.225.0 reason, or forwarded to an alias or address; \
1188 hanging up on user input",
1189 )
1190 .e2e_testing(
1191 "tests/server/h323: Asterisk's chan_ooh323 (ooh323c) and H323Plus's simph323 \
1192 call NetGet's endpoint and see the call connected with fastStart media, or \
1193 released with the model's cause; tshark's q931, h225 and h245 dissectors read \
1194 every message both ways from a live capture",
1195 )
1196 .notes(
1197 "Experimental: two independent H.323 stacks (ooh323c and H323Plus) complete \
1198 calls with it in tests that fail rather than skip and the PER decoder has a fuzz \
1199 target (fuzz/fuzz_targets/h323_per.rs), but nobody has reviewed the H.245 \
1200 subset against the specification and its pair job had not run in CI when it \
1201 landed. No separate H.245 channel (tunnelling or fastStart only; a \
1202 caller that offers neither still connects at the Q.931 level, with no media \
1203 negotiated). G.711 only. No media is sent and none is decoded. No H.235 \
1204 security, no H.450 supplementary services, no overlap sending.",
1205 )
1206 .build()
1207 }
1208
1209 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
1210 vec![codecs_parameter()]
1211 }
1212
1213 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
1214 Vec::new()
1215 }
1216
1217 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
1218 endpoint_actions()
1219 }
1220
1221 fn get_event_types(&self) -> Vec<EventType> {
1222 vec![SETUP_EVENT.clone(), USER_INPUT_EVENT.clone()]
1223 }
1224
1225 fn get_startup_examples(&self) -> StartupExamples {
1226 StartupExamples::new(
1227 json!({
1228 "type": "open_server",
1229 "port": 1720,
1230 "base_stack": "H323",
1231 "instruction": "Answer calls whose called number or destination alias is 1001. Reject everything else with cause 17 (busy)."
1232 }),
1233 json!({
1234 "type": "open_server",
1235 "port": 1720,
1236 "base_stack": "H323",
1237 "event_handlers": [{
1238 "event_pattern": "h323_setup",
1239 "handler": {
1240 "type": "script",
1241 "language": "python",
1242 "code": "import json,sys\ne=json.load(sys.stdin)['event']\nwanted='dialledDigits:1001' in e.get('destination_aliases',[]) or e.get('called_number')=='1001'\na=[{'type':'h323_alerting'},{'type':'h323_connect'}] if wanted else [{'type':'h323_release_complete','cause':17,'reason':'inConf'}]\nprint(json.dumps({'actions':a}))"
1243 }
1244 }, {"event_pattern": "*", "handler": {"type": "static", "actions": []}}]
1245 }),
1246 json!({
1247 "type": "open_server",
1248 "port": 1720,
1249 "base_stack": "H323",
1250 "event_handlers": [
1251 {"event_pattern": "h323_setup", "handler": {"type": "static", "actions": [
1252 {"type": CALL_PROCEEDING}, {"type": ALERTING}, {"type": CONNECT, "codec": "g711u"}
1253 ]}},
1254 {"event_pattern": "*", "handler": {"type": "static", "actions": []}}
1255 ]
1256 }),
1257 )
1258 }
1259}
1260
1261impl Server for H323EndpointProtocol {
1262 fn spawn(
1263 &self,
1264 ctx: crate::protocol::SpawnContext,
1265 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<SocketAddr>> + Send>> {
1266 Box::pin(async move {
1267 let codecs = codecs_from_params(ctx.startup_params.as_ref())?;
1268 endpoint::spawn(
1269 ctx.legacy_listen_addr(),
1270 ctx.llm_client,
1271 ctx.state,
1272 ctx.status_tx,
1273 ctx.server_id,
1274 endpoint::EndpointConfig { codecs },
1275 )
1276 .await
1277 })
1278 }
1279
1280 fn execute_action(&self, action: Value) -> Result<ActionResult> {
1281 parse_call_command(&action)?;
1282 Ok(ActionResult::Custom {
1283 name: action["type"].as_str().unwrap_or_default().to_string(),
1284 data: action,
1285 })
1286 }
1287}