1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub struct SipProtocol;
15
16impl SipProtocol {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22impl Protocol for SipProtocol {
24 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
38 vec![]
39 }
40 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
41 vec![
42 sip_register_action(),
43 sip_invite_action(),
44 sip_bye_action(),
45 sip_ack_action(),
46 sip_options_action(),
47 sip_cancel_action(),
48 ]
49 }
50 fn protocol_name(&self) -> &'static str {
51 "SIP"
52 }
53 fn get_event_types(&self) -> Vec<EventType> {
54 get_sip_event_types()
55 }
56 fn stack_name(&self) -> &'static str {
57 "ETH>IP>UDP>SIP"
58 }
59 fn keywords(&self) -> Vec<&'static str> {
60 vec!["sip", "voip", "session initiation"]
61 }
62 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
63 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
64
65 ProtocolMetadataV2::builder()
66 .connectionless()
67 .answers_on_failure()
70 .state(DevelopmentState::Beta)
71 .well_known_udp_port(5060)
72 .implementation(
78 "Manual line-based parser and response builder (no SIP library; Cargo declares \
79 `sip = []`). Request line + headers + optional SDP body only: no transaction \
80 layer, no retransmissions, no digest auth, no dialog state, UDP only",
81 )
82 .llm_control("Registration decisions + call routing + SDP generation")
83 .e2e_testing(
88 "REAL THIRD-PARTY CLIENT: sipsak 0.9.8.1 (the FhG Fokus SER torture-tester, a \
89 C program) in tests/server/sip/real_client_test.rs. NOT #[ignore]d and NOT \
90 skip-gated — the tests FAIL, naming `brew install sipsak`, when the binary is \
91 absent. Not circular: `sip = []` in Cargo.toml, so this server is hand-written \
92 over tokio's UdpSocket with no SIP library at all. Three transactions: an \
93 OPTIONS answered 200 with the model's own allow_methods (sipsak exits 0 only \
94 after matching the reply's Via+branch, Call-ID and CSeq against the request it \
95 sent — the correlation a dissector cannot check, and the thing that decides \
96 whether a real UA accepts a reply or discards it as unmatched); the same \
97 OPTIONS answered 403, where sipsak reports the code and exits non-zero, \
98 proving it reads our status line rather than merely receiving a datagram; and \
99 a REGISTER through usrloc mode, which sipsak reports as 'All usrloc tests \
100 completed successful.' Since build_sip_response is ONE function for every \
101 method, the correlation proven on OPTIONS is the same code every reply uses. \
102 Alongside: the mocked-LLM UDP e2e over the six methods, whose replies are \
103 additionally checked with the Wireshark `sip` dissector, plus a fail-closed \
104 503/ACK-silence test and an RTP interop test. \
105 NOT PROVEN: INVITE and its SDP against a real UA (sipsak's -I mode calls \
106 itself and needs a media stack; no softphone is driven here), so call setup is \
107 covered only by the dissector; digest authentication, which is NOT IMPLEMENTED \
108 and which no action can produce, so no client can be made to authenticate; \
109 TCP and TLS transport (UDP only); dialogs, CANCEL, and multi-Via proxy paths. \
110 Also note sipsak's usrloc mode deactivates Via insertion, so the REGISTER test \
111 contributes the admission decision and NOT correlation evidence.",
112 )
113 .notes(
114 "Scripting candidate, VoIP signaling honeypot. No registration database: the \
115 model decides each REGISTER on its own and nothing is stored between requests \
116 (protocols must not implement storage). REGISTER and INVITE are admission \
117 decisions and fail closed - an LLM error is 503, an action with no \
118 status_code or no SIP response action at all is 500, never a defaulted 200",
119 )
120 .build()
121 }
122 fn description(&self) -> &'static str {
123 "SIP server for VoIP signaling"
124 }
125 fn example_prompt(&self) -> &'static str {
126 "Start a SIP server on port 5060 for VoIP registration and call signaling"
127 }
128 fn group_name(&self) -> &'static str {
129 "Proxy & Network"
130 }
131
132 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
133 use crate::llm::actions::StartupExamples;
134
135 let script = r#"import json, sys
138data = json.load(sys.stdin)
139event = data["event"]
140et = data["event_type_id"]
141if et == "sip_register":
142 actions = [{"type": "sip_register", "status_code": 200}]
143elif et == "sip_invite":
144 actions = [{"type": "sip_invite", "status_code": 200}]
145elif et == "sip_options":
146 actions = [{"type": "sip_options", "status_code": 200}]
147else:
148 actions = []
149print(json.dumps({"actions": actions}))"#;
150
151 StartupExamples::new(
152 json!({
154 "type": "open_server",
155 "port": 5060,
156 "base_stack": "sip",
157 "instruction": "SIP VoIP signaling server. Accept REGISTER requests with 200 OK. For INVITE requests, respond with 200 OK and SDP. Log all call setup attempts."
158 }),
159 json!({
161 "type": "open_server",
162 "port": 5060,
163 "base_stack": "sip",
164 "event_handlers": [{
165 "event_pattern": "sip_register",
166 "handler": {
167 "type": "script",
168 "language": "python",
169 "code": script
170 }
171 }, {
172 "event_pattern": "sip_invite",
173 "handler": {
174 "type": "script",
175 "language": "python",
176 "code": script
177 }
178 }, {
179 "event_pattern": "sip_options",
180 "handler": {
181 "type": "script",
182 "language": "python",
183 "code": script
184 }
185 }]
186 }),
187 json!({
189 "type": "open_server",
190 "port": 5060,
191 "base_stack": "sip",
192 "event_handlers": [{
193 "event_pattern": "sip_register",
194 "handler": {
195 "type": "static",
196 "actions": [{
197 "type": "sip_register",
198 "status_code": 200,
199 "expires": 3600
200 }]
201 }
202 }]
203 }),
204 )
205 }
206}
207
208impl Server for SipProtocol {
210 fn spawn(
211 &self,
212 ctx: crate::protocol::SpawnContext,
213 ) -> std::pin::Pin<
214 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
215 > {
216 Box::pin(async move {
217 use crate::server::sip::SipServer;
218 SipServer::spawn_with_llm_actions(
219 ctx.legacy_listen_addr(),
220 ctx.llm_client,
221 ctx.state,
222 ctx.status_tx,
223 ctx.server_id,
224 )
225 .await
226 })
227 }
228 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
229 let action_type = action
230 .get("type")
231 .and_then(|v| v.as_str())
232 .context("Missing 'type' field in action")?;
233
234 match action_type {
235 "sip_register" => self.execute_sip_register(action),
236 "sip_invite" => self.execute_sip_invite(action),
237 "sip_bye" => self.execute_sip_bye(action),
238 "sip_ack" => Ok(ActionResult::NoAction), "sip_options" => self.execute_sip_options(action),
240 "sip_cancel" => self.execute_sip_cancel(action),
241 _ => Err(anyhow::anyhow!("Unknown SIP action: {}", action_type)),
242 }
243 }
244}
245
246impl SipProtocol {
247 fn execute_sip_register(&self, _action: serde_json::Value) -> Result<ActionResult> {
250 Ok(ActionResult::NoAction)
252 }
253
254 fn execute_sip_invite(&self, _action: serde_json::Value) -> Result<ActionResult> {
256 Ok(ActionResult::NoAction)
257 }
258
259 fn execute_sip_bye(&self, _action: serde_json::Value) -> Result<ActionResult> {
261 Ok(ActionResult::NoAction)
262 }
263
264 fn execute_sip_options(&self, _action: serde_json::Value) -> Result<ActionResult> {
266 Ok(ActionResult::NoAction)
267 }
268
269 fn execute_sip_cancel(&self, _action: serde_json::Value) -> Result<ActionResult> {
271 Ok(ActionResult::NoAction)
272 }
273}
274
275fn get_sip_event_types() -> Vec<EventType> {
277 vec![
278 SIP_REGISTER_EVENT.clone(),
279 SIP_INVITE_EVENT.clone(),
280 SIP_BYE_EVENT.clone(),
281 SIP_ACK_EVENT.clone(),
282 SIP_OPTIONS_EVENT.clone(),
283 SIP_CANCEL_EVENT.clone(),
284 ]
285}
286
287pub static SIP_REGISTER_EVENT: LazyLock<EventType> = LazyLock::new(|| {
289 EventType::new(
290 "sip_register",
291 "A client is registering its location with REGISTER. Answer with sip_register: 200 to \
292 accept the binding, 401/403 to challenge or refuse it.",
293 json!({
294 "type": "sip_register",
295 "status_code": 200,
296 "expires": 3600
297 }),
298 )
299 .with_actions(vec![sip_register_action()])
304 .with_log_template(
305 LogTemplate::new()
306 .with_info("SIP REGISTER")
307 .with_debug("SIP REGISTER request")
308 .with_trace("SIP: {json_pretty(.)}"),
309 )
310});
311
312pub static SIP_INVITE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
314 EventType::new(
315 "sip_invite",
316 "A client is initiating a session with INVITE. Answer with sip_invite carrying the status \
317 code and, for a 200, the answering SDP.",
318 json!({
319 "type": "sip_invite",
320 "status_code": 200,
321 "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=Call\nc=IN IP4 127.0.0.1\nt=0 0\nm=audio 8000 RTP/AVP 0\n"
322 }),
323 )
324 .with_actions(vec![sip_invite_action()])
325 .with_log_template(
326 LogTemplate::new()
327 .with_info("SIP INVITE")
328 .with_debug("SIP INVITE request")
329 .with_trace("SIP: {json_pretty(.)}"),
330 )
331});
332
333pub static SIP_BYE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
335 EventType::new(
336 "sip_bye",
337 "The far end is terminating the session with BYE. Answer with sip_bye; 200 acknowledges \
338 the teardown.",
339 json!({"type": "sip_bye", "status_code": 200}),
340 )
341 .with_actions(vec![sip_bye_action()])
342 .with_log_template(
343 LogTemplate::new()
344 .with_info("SIP BYE")
345 .with_debug("SIP BYE request")
346 .with_trace("SIP: {json_pretty(.)}"),
347 )
348});
349
350pub static SIP_ACK_EVENT: LazyLock<EventType> = LazyLock::new(|| {
352 EventType::new(
353 "sip_ack",
354 "The client acknowledged an INVITE response. ACK is never answered on the wire, so \
355 sip_ack simply records that the dialog is established.",
356 json!({"type": "sip_ack"}),
357 )
358 .with_actions(vec![sip_ack_action()])
359 .with_log_template(
360 LogTemplate::new()
361 .with_info("SIP ACK")
362 .with_debug("SIP ACK request")
363 .with_trace("SIP: {json_pretty(.)}"),
364 )
365});
366
367pub fn default_reason_phrase(code: u16) -> &'static str {
371 match code {
372 100 => "Trying",
373 180 => "Ringing",
374 181 => "Call Is Being Forwarded",
375 182 => "Queued",
376 183 => "Session Progress",
377 200 => "OK",
378 202 => "Accepted",
379 300 => "Multiple Choices",
380 301 => "Moved Permanently",
381 302 => "Moved Temporarily",
382 305 => "Use Proxy",
383 380 => "Alternative Service",
384 400 => "Bad Request",
385 401 => "Unauthorized",
386 402 => "Payment Required",
387 403 => "Forbidden",
388 404 => "Not Found",
389 405 => "Method Not Allowed",
390 406 => "Not Acceptable",
391 407 => "Proxy Authentication Required",
392 408 => "Request Timeout",
393 410 => "Gone",
394 413 => "Request Entity Too Large",
395 414 => "Request-URI Too Long",
396 415 => "Unsupported Media Type",
397 416 => "Unsupported URI Scheme",
398 420 => "Bad Extension",
399 421 => "Extension Required",
400 423 => "Interval Too Brief",
401 480 => "Temporarily Unavailable",
402 481 => "Call/Transaction Does Not Exist",
403 482 => "Loop Detected",
404 483 => "Too Many Hops",
405 484 => "Address Incomplete",
406 485 => "Ambiguous",
407 486 => "Busy Here",
408 487 => "Request Terminated",
409 488 => "Not Acceptable Here",
410 491 => "Request Pending",
411 493 => "Undecipherable",
412 500 => "Server Internal Error",
413 501 => "Not Implemented",
414 502 => "Bad Gateway",
415 503 => "Service Unavailable",
416 504 => "Server Time-out",
417 505 => "Version Not Supported",
418 513 => "Message Too Large",
419 600 => "Busy Everywhere",
420 603 => "Decline",
421 604 => "Does Not Exist Anywhere",
422 606 => "Not Acceptable",
423 100..=199 => "Session Progress",
424 200..=299 => "OK",
425 300..=399 => "Redirection",
426 400..=499 => "Request Failure",
427 500..=599 => "Server Failure",
428 _ => "Global Failure",
429 }
430}
431
432pub const OPTIONS_ANSWER_WITH: &str = "sip_options with the status an INVITE would get right \
440 now: status_code 200 if your instructions say this endpoint is available to take a call; \
441 486 (Busy Here) if they say it is busy, on another call or in do-not-disturb; 480 \
442 (Temporarily Unavailable) if they say it is offline or away; 603 (Decline) if it refuses \
443 every call";
444
445pub static SIP_OPTIONS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
447 EventType::new(
448 "sip_options",
449 "A client is checking on this endpoint with OPTIONS - whether it is reachable and \
450 what it supports. RFC 3261 section 11.2: the status is the one an INVITE would get \
451 right now, so 200 only when the endpoint would take a call; answer_with says which \
452 code fits which situation.",
453 json!({
454 "type": "sip_options",
455 "status_code": 200,
456 "allow_methods": ["INVITE", "ACK", "BYE", "REGISTER", "OPTIONS"]
457 }),
458 )
459 .with_parameters(vec![Parameter {
460 name: "answer_with".to_string(),
461 type_hint: "string".to_string(),
462 description: "Which status this OPTIONS takes: the one an INVITE would get now - 200 \
463 available, 486 busy or do-not-disturb, 480 offline. Present for an \
464 OPTIONS request; absent for a method this server does not recognise"
465 .to_string(),
466 required: false,
467 }])
468 .with_actions(vec![sip_options_action()])
469 .with_log_template(
470 LogTemplate::new()
471 .with_info("SIP OPTIONS")
472 .with_debug("SIP OPTIONS request")
473 .with_trace("SIP: {json_pretty(.)}"),
474 )
475});
476
477pub static SIP_CANCEL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
479 EventType::new(
480 "sip_cancel",
481 "The client is cancelling a pending INVITE. Answer with sip_cancel; 200 confirms the \
482 cancellation.",
483 json!({"type": "sip_cancel", "status_code": 200}),
484 )
485 .with_actions(vec![sip_cancel_action()])
486 .with_log_template(
487 LogTemplate::new()
488 .with_info("SIP CANCEL")
489 .with_debug("SIP CANCEL request")
490 .with_trace("SIP: {json_pretty(.)}"),
491 )
492});
493
494fn sip_register_action() -> ActionDefinition {
496 ActionDefinition {
497 name: "sip_register".to_string(),
498 description: "Respond to SIP REGISTER request".to_string(),
499 parameters: vec![
500 Parameter {
501 name: "status_code".to_string(),
502 type_hint: "number".to_string(),
503 description: "SIP status code (200=OK, 403=Forbidden, 401=Unauthorized)"
504 .to_string(),
505 required: true,
506 },
507 Parameter {
508 name: "reason_phrase".to_string(),
509 type_hint: "string".to_string(),
510 description: "Optional reason phrase (default based on status code)".to_string(),
511 required: false,
512 },
513 Parameter {
514 name: "expires".to_string(),
515 type_hint: "number".to_string(),
516 description: "Registration expiration in seconds (default 3600)".to_string(),
517 required: false,
518 },
519 ],
520 example: json!({
521 "type": "sip_register",
522 "status_code": 200,
523 "expires": 3600
524 }),
525 log_template: Some(
526 LogTemplate::new()
527 .with_info("-> SIP {status_code} REGISTER")
528 .with_debug("SIP sip_register: status={status_code}, expires={expires}"),
529 ),
530 }
531}
532
533fn sip_invite_action() -> ActionDefinition {
534 ActionDefinition {
535 name: "sip_invite".to_string(),
536 description: "Respond to SIP INVITE request".to_string(),
537 parameters: vec![
538 Parameter {
539 name: "status_code".to_string(),
540 type_hint: "number".to_string(),
541 description: "SIP status code (200=OK, 486=Busy, 603=Decline, 180=Ringing)"
542 .to_string(),
543 required: true,
544 },
545 Parameter {
546 name: "reason_phrase".to_string(),
547 type_hint: "string".to_string(),
548 description: "Optional reason phrase".to_string(),
549 required: false,
550 },
551 Parameter {
552 name: "sdp".to_string(),
553 type_hint: "string".to_string(),
554 description: "Session Description Protocol body (required for 200 OK)".to_string(),
555 required: false,
556 },
557 Parameter {
558 name: "rtp_audio".to_string(),
559 type_hint: "object".to_string(),
560 description: "Optional media to actually stream on a 200 OK: {content, tone_hz, \
561 payload_type, duration_ms}. When NetGet is built with the `rtp` \
562 feature, accepting a call streams this as real RTP to the m=audio \
563 target in the caller's INVITE SDP, so the negotiated session carries \
564 media rather than only promising it. Ignored without the `rtp` feature."
565 .to_string(),
566 required: false,
567 },
568 ],
569 example: json!({
570 "type": "sip_invite",
571 "status_code": 200,
572 "sdp": "v=0\no=- 0 0 IN IP4 127.0.0.1\ns=Call\nc=IN IP4 127.0.0.1\nt=0 0\nm=audio 8000 RTP/AVP 0\n",
573 "rtp_audio": {"content": "tone", "tone_hz": 440, "payload_type": "pcmu", "duration_ms": 2000}
574 }),
575 log_template: Some(
576 LogTemplate::new()
577 .with_info("-> SIP {status_code} INVITE")
578 .with_debug("SIP sip_invite: status={status_code}"),
579 ),
580 }
581}
582
583fn sip_bye_action() -> ActionDefinition {
584 ActionDefinition {
585 name: "sip_bye".to_string(),
586 description: "Respond to SIP BYE request".to_string(),
587 parameters: vec![Parameter {
588 name: "status_code".to_string(),
589 type_hint: "number".to_string(),
590 description: "SIP status code; 200 acknowledges the teardown. Required - an action \
595 with no status_code is answered 500, not 200"
596 .to_string(),
597 required: true,
598 }],
599 example: json!({
600 "type": "sip_bye",
601 "status_code": 200
602 }),
603 log_template: Some(
604 LogTemplate::new()
605 .with_info("-> SIP {status_code} BYE")
606 .with_debug("SIP sip_bye: status={status_code}"),
607 ),
608 }
609}
610
611fn sip_ack_action() -> ActionDefinition {
612 ActionDefinition {
613 name: "sip_ack".to_string(),
614 description: "Process SIP ACK (no response needed)".to_string(),
615 parameters: vec![],
616 example: json!({
617 "type": "sip_ack"
618 }),
619 log_template: Some(
620 LogTemplate::new()
621 .with_info("-> SIP ACK processed")
622 .with_debug("SIP sip_ack"),
623 ),
624 }
625}
626
627fn sip_options_action() -> ActionDefinition {
628 ActionDefinition {
629 name: "sip_options".to_string(),
630 description: "Answer an OPTIONS request. Its status is the one an INVITE would get \
631 right now (RFC 3261 section 11.2): 200 when this endpoint is available, \
632 486 Busy Here when it is busy or in do-not-disturb, 480 Temporarily \
633 Unavailable when it is offline, 600 Busy Everywhere or 603 Decline when \
634 it refuses every call."
635 .to_string(),
636 parameters: vec![
637 Parameter {
638 name: "status_code".to_string(),
639 type_hint: "number".to_string(),
640 description: "SIP status code: 200 available, 486 busy / do-not-disturb, 480 \
641 unavailable, 603 decline. Required - an action with no \
642 status_code is answered 500, not 200"
643 .to_string(),
644 required: true,
645 },
646 Parameter {
647 name: "allow_methods".to_string(),
648 type_hint: "array".to_string(),
649 description: "Array of supported SIP methods".to_string(),
650 required: false,
651 },
652 ],
653 example: json!({
654 "type": "sip_options",
655 "status_code": 200,
656 "allow_methods": ["INVITE", "ACK", "BYE", "REGISTER", "OPTIONS"]
657 }),
658 log_template: Some(
659 LogTemplate::new()
660 .with_info("-> SIP {status_code} OPTIONS")
661 .with_debug("SIP sip_options: status={status_code}, methods={allow_methods_len}"),
662 ),
663 }
664}
665
666fn sip_cancel_action() -> ActionDefinition {
667 ActionDefinition {
668 name: "sip_cancel".to_string(),
669 description: "Respond to SIP CANCEL request".to_string(),
670 parameters: vec![Parameter {
671 name: "status_code".to_string(),
672 type_hint: "number".to_string(),
673 description: "SIP status code; 200 confirms the cancellation. Required - an action \
674 with no status_code is answered 500, not 200"
675 .to_string(),
676 required: true,
677 }],
678 example: json!({
679 "type": "sip_cancel",
680 "status_code": 200
681 }),
682 log_template: Some(
683 LogTemplate::new()
684 .with_info("-> SIP {status_code} CANCEL")
685 .with_debug("SIP sip_cancel: status={status_code}"),
686 ),
687 }
688}
689
690