netget/client/sip/mod.rs
1//! SIP client implementation
2pub mod actions;
3
4pub use actions::SipClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::net::UdpSocket;
10use tokio::sync::{mpsc, Mutex};
11use tracing::{error, info, trace, warn};
12
13use crate::client::llm_budget::call_llm_for_client;
14use crate::client::sip::actions::{SIP_CLIENT_CONNECTED_EVENT, SIP_CLIENT_RESPONSE_RECEIVED_EVENT};
15use crate::llm::ollama_client::OllamaClient;
16use crate::llm::ClientLlmResult;
17use crate::protocol::Event;
18use crate::state::app_state::AppState;
19use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
20use crate::state::{AccessLogOwner, ClientId, ClientStatus};
21
22/// Connection state for LLM processing
23#[derive(Debug, Clone, PartialEq)]
24enum ConnectionState {
25 Idle,
26 Processing,
27 /// Reserved for future TCP support where partial messages may need buffering
28 #[allow(dead_code)]
29 Accumulating,
30}
31
32/// Per-client data for LLM handling
33struct ClientData {
34 state: ConnectionState,
35 queued_data: Vec<u8>,
36 memory: String,
37 call_id: Option<String>,
38 from_tag: Option<String>,
39 to_tag: Option<String>,
40 cseq: u32,
41 /// This socket's own address, for Via and Contact. Held here because
42 /// `execute_sip_action` is reached from three places (connect, read loop, injected
43 /// command) and all three already take the `ClientData` lock to build a request.
44 local_addr: SocketAddr,
45}
46
47/// SIP client that connects to a remote SIP server
48pub struct SipClient;
49
50impl SipClient {
51 /// Connect to a SIP server with integrated LLM actions
52 pub async fn connect_with_llm_actions(
53 remote_addr: String,
54 llm_client: OllamaClient,
55 app_state: Arc<AppState>,
56 status_tx: mpsc::UnboundedSender<String>,
57 client_id: ClientId,
58 ) -> Result<SocketAddr> {
59 // Parse remote address
60 let remote_sock_addr: SocketAddr = remote_addr
61 .parse()
62 .context(format!("Invalid SIP server address: {}", remote_addr))?;
63
64 // Bind to local UDP socket (ephemeral port)
65 let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?);
66
67 // Connect UDP socket to remote address
68 socket.connect(remote_sock_addr).await?;
69
70 // Read the local address *after* connect. Before it, the socket is bound to
71 // 0.0.0.0 and `local_addr()` says so; after it, the kernel has picked the outbound
72 // interface and reports the address a peer would actually see. This is the address
73 // that goes in Via and Contact, so taking it a line too early put a wildcard on the
74 // wire.
75 let local_addr = socket.local_addr()?;
76
77 info!(
78 "SIP client {} connected to {} (local: {})",
79 client_id, remote_sock_addr, local_addr
80 );
81
82 // Update client state
83 app_state
84 .update_client_status(client_id, ClientStatus::Connected)
85 .await;
86 let _ = status_tx.send(format!("[CLIENT] SIP client {} connected", client_id));
87 let _ = status_tx.send("__UPDATE_UI__".to_string());
88
89 // Initialize client data
90 let client_data = Arc::new(Mutex::new(ClientData {
91 state: ConnectionState::Idle,
92 queued_data: Vec::new(),
93 memory: String::new(),
94 call_id: None,
95 from_tag: None,
96 to_tag: None,
97 cseq: 1,
98 local_addr,
99 }));
100
101 // Call LLM with initial connected event
102 let protocol = Arc::new(crate::client::sip::actions::SipClientProtocol::new());
103 let event = Event::new(
104 &SIP_CLIENT_CONNECTED_EVENT,
105 serde_json::json!({
106 "remote_addr": remote_sock_addr.to_string(),
107 "local_addr": local_addr.to_string(),
108 }),
109 );
110
111 // Command channel for injected actions (the dashboard's [ send ]).
112 // Registered BEFORE the connected-event LLM call: a manual `*` routing rule can
113 // park that call for minutes, and the operator must be able to reach the client
114 // while it waits. `recv` is cancellation-safe, so a `select!` arm in the read loop
115 // would also be sound - but the read loop can sit inside a multi-minute LLM call,
116 // and injected commands must not queue behind it. Hence a separate task; the UDP
117 // socket is an `Arc<UdpSocket>` and `send` takes `&self`, so both tasks can write.
118 let command_rx =
119 crate::client::command_support::register_command_channel(&app_state, client_id).await;
120 let cmd_task = tokio::spawn(Self::command_loop(
121 command_rx,
122 socket.clone(),
123 protocol.clone(),
124 client_data.clone(),
125 client_id,
126 app_state.clone(),
127 status_tx.clone(),
128 ));
129 app_state.register_client_task(client_id, cmd_task).await;
130
131 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
132 // Copy the memory out and drop the guard: passing `&guard.memory` straight into the
133 // call would hold the mutex for the whole LLM round-trip, and an injected command
134 // (which needs the same mutex for the Call-ID / tag / CSeq state) would queue behind
135 // it - defeating the point of running the command loop as its own task.
136 let memory = client_data.lock().await.memory.clone();
137 match call_llm_for_client(
138 &llm_client,
139 &app_state,
140 client_id.to_string(),
141 &instruction,
142 &memory,
143 Some(&event),
144 protocol.as_ref(),
145 &status_tx,
146 )
147 .await
148 {
149 Ok(ClientLlmResult {
150 actions,
151 memory_updates,
152 }) => {
153 // Update memory
154 if let Some(mem) = memory_updates {
155 client_data.lock().await.memory = mem;
156 }
157
158 // Execute initial actions (e.g., REGISTER)
159 for action in actions {
160 if let Err(e) = Self::execute_sip_action(
161 action,
162 &socket,
163 &protocol,
164 &client_data,
165 client_id,
166 &status_tx,
167 )
168 .await
169 {
170 error!("SIP client {} send error: {}", client_id, e);
171 }
172 }
173 }
174 Err(e) => {
175 error!("LLM error for SIP client {}: {}", client_id, e);
176 }
177 }
178 }
179
180 // Spawn read loop
181 let socket_clone = socket.clone();
182 let llm_clone = llm_client.clone();
183 let state_clone = app_state.clone();
184 let status_clone = status_tx.clone();
185 let protocol_clone = protocol.clone();
186 let client_data_clone = client_data.clone();
187
188 // Registered with AppState so stop_client can abort this task —
189 // dropping a JoinHandle only detaches it in Tokio.
190 let task_registrar = app_state.clone();
191 let task_handle = tokio::spawn(async move {
192 let mut buffer = vec![0u8; 65535]; // Max UDP packet size
193
194 loop {
195 match socket_clone.recv(&mut buffer).await {
196 Ok(n) => {
197 let data = buffer[..n].to_vec();
198 trace!("SIP client {} received {} bytes", client_id, n);
199
200 // Parse SIP response
201 let response = match Self::parse_sip_response(&data) {
202 Ok(resp) => resp,
203 Err(e) => {
204 warn!("SIP client {} failed to parse response: {}", client_id, e);
205 continue;
206 }
207 };
208
209 info!(
210 "SIP client {} received {} response (status: {})",
211 client_id, response.method, response.status_code
212 );
213
214 // Handle provisional responses (1xx) without calling LLM
215 if response.status_code >= 100 && response.status_code < 200 {
216 info!(
217 "SIP client {} received provisional response {} {}, skipping LLM",
218 client_id, response.status_code, response.reason_phrase
219 );
220 let _ = status_clone.send(format!(
221 "[CLIENT] SIP {} response: {} {}",
222 client_id, response.status_code, response.reason_phrase
223 ));
224 continue;
225 }
226
227 // Handle response with LLM
228 let mut client_data_lock = client_data_clone.lock().await;
229
230 match client_data_lock.state {
231 ConnectionState::Idle => {
232 // Process immediately
233 client_data_lock.state = ConnectionState::Processing;
234
235 // Extract To tag if present
236 if let Some(to_tag) = &response.to_tag {
237 client_data_lock.to_tag = Some(to_tag.clone());
238 }
239
240 drop(client_data_lock);
241
242 // Call LLM
243 if let Some(instruction) =
244 state_clone.get_instruction_for_client(client_id).await
245 {
246 let event = Event::new(
247 &SIP_CLIENT_RESPONSE_RECEIVED_EVENT,
248 serde_json::json!({
249 "status_code": response.status_code,
250 "reason_phrase": response.reason_phrase,
251 "method": response.method,
252 "call_id": response.call_id,
253 "from": response.from,
254 "to": response.to,
255 "body": response.body,
256 }),
257 );
258
259 // Copied out so the mutex is not held across the LLM
260 // round-trip (see the connect path).
261 let memory = client_data_clone.lock().await.memory.clone();
262 match call_llm_for_client(
263 &llm_clone,
264 &state_clone,
265 client_id.to_string(),
266 &instruction,
267 &memory,
268 Some(&event),
269 protocol_clone.as_ref(),
270 &status_clone,
271 )
272 .await
273 {
274 Ok(ClientLlmResult {
275 actions,
276 memory_updates,
277 }) => {
278 // Update memory
279 if let Some(mem) = memory_updates {
280 client_data_clone.lock().await.memory = mem;
281 }
282
283 // Execute actions
284 for action in actions {
285 if let Err(e) = Self::execute_sip_action(
286 action,
287 &socket_clone,
288 &protocol_clone,
289 &client_data_clone,
290 client_id,
291 &status_clone,
292 )
293 .await
294 {
295 error!(
296 "SIP client {} send error: {}",
297 client_id, e
298 );
299 }
300 }
301 }
302 Err(e) => {
303 error!("LLM error for SIP client {}: {}", client_id, e);
304 }
305 }
306 }
307
308 // Send automatic ACK for 200 OK response to INVITE (RFC 3261)
309 if response.method == "INVITE" && response.status_code == 200 {
310 info!(
311 "SIP client {} sending automatic ACK for INVITE 200 OK",
312 client_id
313 );
314
315 // Both of these were `.unwrap()`, and both are `None` until
316 // this client has itself sent a request. A datagram whose
317 // CSeq says `INVITE` and whose status line says 200 — which
318 // any peer can send unprompted — therefore panicked the read
319 // loop, inside a `tokio::spawn` that swallows the panic: the
320 // client stayed "Connected" and went permanently deaf.
321 // There is no dialog to acknowledge in that case, so the
322 // honest answer is to log it and send nothing.
323 let ack = {
324 let d = client_data_clone.lock().await;
325 match (d.call_id.as_ref(), d.from_tag.as_ref()) {
326 (Some(call_id), Some(from_tag)) => {
327 Some(Self::build_ack_request(
328 &response,
329 call_id,
330 from_tag,
331 // Same CSeq as the INVITE. Saturating
332 // because `cseq` is unsigned and nothing
333 // guarantees a request preceded this.
334 d.cseq.saturating_sub(1),
335 d.local_addr,
336 ))
337 }
338 _ => None,
339 }
340 };
341 match ack {
342 None => warn!(
343 "SIP client {} got an INVITE 200 OK with no local \
344 dialog (no Call-ID or From tag yet); this client \
345 sent no INVITE, so no ACK is owed",
346 client_id
347 ),
348 Some(ack_request) => {
349 match socket_clone.send(&ack_request).await {
350 Ok(sent) => {
351 info!(
352 "SIP client {} sent ACK ({} bytes)",
353 client_id, sent
354 );
355 let _ = status_clone.send(format!(
356 "[CLIENT] SIP client {} sent ACK",
357 client_id
358 ));
359 }
360 Err(e) => {
361 error!(
362 "SIP client {} ACK send error: {}",
363 client_id, e
364 );
365 }
366 }
367 }
368 }
369 }
370
371 // Reset state
372 let mut lock = client_data_clone.lock().await;
373 lock.state = ConnectionState::Idle;
374
375 // Process queued data if any
376 if !lock.queued_data.is_empty() {
377 let queued = lock.queued_data.clone();
378 lock.queued_data.clear();
379 drop(lock);
380
381 info!(
382 "SIP client {} processing {} bytes of queued data",
383 client_id,
384 queued.len()
385 );
386
387 // Parse and process queued response
388 if let Ok(queued_response) = Self::parse_sip_response(&queued) {
389 // Skip provisional responses in queue
390 if queued_response.status_code >= 200 {
391 // Set state to Processing for queued response
392 client_data_clone.lock().await.state =
393 ConnectionState::Processing;
394
395 // Extract To tag if present
396 if let Some(to_tag) = &queued_response.to_tag {
397 client_data_clone.lock().await.to_tag =
398 Some(to_tag.clone());
399 }
400
401 // Call LLM for queued response
402 if let Some(instruction) = state_clone
403 .get_instruction_for_client(client_id)
404 .await
405 {
406 let event = Event::new(
407 &SIP_CLIENT_RESPONSE_RECEIVED_EVENT,
408 serde_json::json!({
409 "status_code": queued_response.status_code,
410 "reason_phrase": queued_response.reason_phrase,
411 "method": queued_response.method,
412 "call_id": queued_response.call_id,
413 "from": queued_response.from,
414 "to": queued_response.to,
415 "body": queued_response.body,
416 }),
417 );
418
419 // Copied out so the mutex is not held across
420 // the LLM round-trip (see the connect path).
421 let memory =
422 client_data_clone.lock().await.memory.clone();
423 match call_llm_for_client(
424 &llm_clone,
425 &state_clone,
426 client_id.to_string(),
427 &instruction,
428 &memory,
429 Some(&event),
430 protocol_clone.as_ref(),
431 &status_clone,
432 )
433 .await
434 {
435 Ok(ClientLlmResult {
436 actions,
437 memory_updates,
438 }) => {
439 if let Some(mem) = memory_updates {
440 client_data_clone.lock().await.memory =
441 mem;
442 }
443
444 for action in actions {
445 if let Err(e) =
446 Self::execute_sip_action(
447 action,
448 &socket_clone,
449 &protocol_clone,
450 &client_data_clone,
451 client_id,
452 &status_clone,
453 )
454 .await
455 {
456 error!(
457 "SIP client {} send error: {}",
458 client_id, e
459 );
460 }
461 }
462 }
463 Err(e) => {
464 error!("LLM error for queued SIP response {}: {}", client_id, e);
465 }
466 }
467 }
468
469 // Reset state after processing queued response
470 client_data_clone.lock().await.state =
471 ConnectionState::Idle;
472 }
473 }
474 }
475 }
476 ConnectionState::Processing => {
477 // Queue data
478 client_data_lock.queued_data.extend_from_slice(&data);
479 }
480 ConnectionState::Accumulating => {
481 // Accumulate data (not used for UDP)
482 }
483 }
484 }
485 Err(e) => {
486 error!("SIP client {} recv error: {}", client_id, e);
487 state_clone
488 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
489 .await;
490 let _ = status_clone
491 .send(format!("[CLIENT] SIP client {} error: {}", client_id, e));
492 let _ = status_clone.send("__UPDATE_UI__".to_string());
493 break;
494 }
495 }
496 }
497 // Every exit path lands here: drop the command handle so the dashboard stops
498 // offering [ send ] on a dead client. This also closes the command channel,
499 // which ends `command_loop`.
500 state_clone.remove_client_handle(client_id).await;
501 let _ = status_clone.send("__UPDATE_UI__".to_string());
502 });
503 task_registrar
504 .register_client_task(client_id, task_handle)
505 .await;
506
507 Ok(local_addr)
508 }
509
510 /// Drain injected commands until the channel closes (client removed, or the read loop
511 /// exited and dropped the handle) or an injected `disconnect` ends the session.
512 ///
513 /// The generic `command_support::handle_stream_client_command` cannot run this client's
514 /// vocabulary - every SIP verb yields `ClientActionResult::Custom` and the request line,
515 /// Call-ID, tags and CSeq come from per-client state - so the action goes through
516 /// [`Self::execute_sip_action`], the same function the LLM path uses, and the outcome is
517 /// recorded and replied exactly the way the generic arm does it.
518 async fn command_loop(
519 mut command_rx: mpsc::Receiver<ClientCommand>,
520 socket: Arc<UdpSocket>,
521 protocol: Arc<SipClientProtocol>,
522 client_data: Arc<Mutex<ClientData>>,
523 client_id: ClientId,
524 app_state: Arc<AppState>,
525 status_tx: mpsc::UnboundedSender<String>,
526 ) {
527 use crate::llm::actions::protocol_trait::Protocol;
528
529 while let Some(command) = command_rx.recv().await {
530 let action = command.action.clone();
531 let outcome = Self::execute_sip_action(
532 action.clone(),
533 &socket,
534 &protocol,
535 &client_data,
536 client_id,
537 &status_tx,
538 )
539 .await;
540
541 let outcome_json = match &outcome {
542 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
543 Err(e) => serde_json::json!({"error": e.to_string()}),
544 };
545 app_state
546 .record_access_log(
547 AccessLogOwner::Client(client_id.as_u32()),
548 protocol.protocol_name(),
549 None,
550 "injected_action",
551 action,
552 vec![outcome_json],
553 )
554 .await;
555
556 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
557 if let Err(e) = &outcome {
558 error!("SIP client {} injected action failed: {}", client_id, e);
559 let _ = status_tx.send(format!(
560 "[WARN] Client {} injected action failed: {}",
561 client_id, e
562 ));
563 }
564 let _ = status_tx.send("__UPDATE_UI__".to_string());
565 crate::client::command_support::reply(command, outcome);
566
567 if disconnect {
568 // SIP over UDP has no connection to close, so the honest end of an injected
569 // `disconnect` is: stop accepting commands, drop the handle, and mark the
570 // client disconnected. The recv loop's socket is released when the client is
571 // removed (`stop_client` aborts both registered tasks).
572 app_state.remove_client_handle(client_id).await;
573 app_state
574 .update_client_status(client_id, ClientStatus::Disconnected)
575 .await;
576 let _ = status_tx.send("__UPDATE_UI__".to_string());
577 break;
578 }
579 }
580 }
581
582 /// Execute one SIP action and report what actually reached the wire.
583 ///
584 /// Shared by the connected-event path, the read loop and injected commands, so the SIP
585 /// request encoding (and the Call-ID / tag / CSeq bookkeeping that goes with it) exists
586 /// exactly once. `Err` means the datagram could not be sent; the outcome variants are
587 /// the truthful report of what happened otherwise.
588 async fn execute_sip_action(
589 action: serde_json::Value,
590 socket: &Arc<UdpSocket>,
591 protocol: &Arc<SipClientProtocol>,
592 client_data: &Arc<Mutex<ClientData>>,
593 client_id: ClientId,
594 status_tx: &mpsc::UnboundedSender<String>,
595 ) -> Result<ClientSendOutcome> {
596 use crate::llm::actions::client_trait::{Client, ClientActionResult};
597
598 let result = match protocol.as_ref().execute_action(action.clone()) {
599 Ok(result) => result,
600 Err(e) => {
601 error!("Action execution error for SIP client {}: {}", client_id, e);
602 return Ok(ClientSendOutcome::Rejected {
603 error: e.to_string(),
604 });
605 }
606 };
607
608 match result {
609 ClientActionResult::Custom { name, data } => {
610 match name.as_str() {
611 "sip_register" | "sip_invite" | "sip_ack" | "sip_bye" | "sip_options"
612 | "sip_cancel" => {
613 // Build and send SIP request
614 let mut client_data_lock = client_data.lock().await;
615
616 // Generate Call-ID if not set
617 if client_data_lock.call_id.is_none() {
618 client_data_lock.call_id = Some(Self::generate_call_id());
619 }
620
621 // Generate From tag if not set
622 if client_data_lock.from_tag.is_none() {
623 client_data_lock.from_tag = Some(Self::generate_tag());
624 }
625
626 // `expect` rather than `unwrap`: both were just filled in above, so
627 // this cannot be reached, and saying so is cheaper than making the
628 // reader prove it.
629 let request = Self::build_sip_request(
630 &name,
631 &data,
632 client_data_lock
633 .call_id
634 .as_ref()
635 .expect("Call-ID generated above"),
636 client_data_lock
637 .from_tag
638 .as_ref()
639 .expect("From tag generated above"),
640 client_data_lock.to_tag.as_ref(),
641 client_data_lock.cseq,
642 client_data_lock.local_addr,
643 );
644
645 client_data_lock.cseq += 1;
646 drop(client_data_lock);
647
648 // Send request
649 let sent = socket
650 .send(&request)
651 .await
652 .with_context(|| format!("SIP client {} send failed", client_id))?;
653 info!("SIP client {} sent {} bytes", client_id, sent);
654 let _ = status_tx.send(format!(
655 "[CLIENT] SIP client {} sent {} request",
656 client_id, name
657 ));
658 Ok(ClientSendOutcome::Sent { bytes_sent: sent })
659 }
660 other => Ok(ClientSendOutcome::Executed {
661 detail: format!("custom result '{other}' builds no SIP request"),
662 }),
663 }
664 }
665 ClientActionResult::Disconnect => {
666 info!("SIP client {} disconnecting", client_id);
667 let _ = status_tx.send(format!("[CLIENT] SIP client {} disconnecting", client_id));
668 Ok(ClientSendOutcome::Disconnected)
669 }
670 ClientActionResult::WaitForMore => {
671 trace!("SIP client {} waiting for more data", client_id);
672 Ok(ClientSendOutcome::Executed {
673 detail: "wait_for_more".to_string(),
674 })
675 }
676 other => Ok(ClientSendOutcome::Executed {
677 detail: format!("{other:?} sends no SIP request"),
678 }),
679 }
680 }
681
682 /// Parse SIP response from bytes
683 fn parse_sip_response(data: &[u8]) -> Result<SipResponse> {
684 let text = String::from_utf8(data.to_vec())?;
685 let lines: Vec<&str> = text.lines().collect();
686
687 if lines.is_empty() {
688 anyhow::bail!("Empty SIP response");
689 }
690
691 // Parse status line (e.g., "SIP/2.0 200 OK")
692 let status_line: Vec<&str> = lines[0].split_whitespace().collect();
693 if status_line.len() < 3 {
694 anyhow::bail!("Invalid SIP status line");
695 }
696
697 let status_code: u16 = status_line[1].parse()?;
698 let reason_phrase = status_line[2..].join(" ");
699
700 // Parse headers
701 let mut call_id = String::new();
702 let mut from = String::new();
703 let mut to = String::new();
704 let mut cseq = String::new();
705 let mut to_tag = None;
706 let mut content_length = 0;
707
708 let mut i = 1;
709 while i < lines.len() {
710 let line = lines[i];
711 if line.is_empty() {
712 // End of headers
713 i += 1;
714 break;
715 }
716
717 if let Some(colon_pos) = line.find(':') {
718 let (header_name, header_value) = line.split_at(colon_pos);
719 let header_value = header_value[1..].trim();
720
721 match header_name.to_lowercase().as_str() {
722 "call-id" => call_id = header_value.to_string(),
723 "from" | "f" => from = header_value.to_string(),
724 "to" | "t" => {
725 to = header_value.to_string();
726 // Extract tag if present
727 if let Some(tag_start) = header_value.find(";tag=") {
728 to_tag = Some(header_value[tag_start + 5..].to_string());
729 }
730 }
731 "cseq" => cseq = header_value.to_string(),
732 "content-length" | "l" => content_length = header_value.parse().unwrap_or(0),
733 _ => {}
734 }
735 }
736
737 i += 1;
738 }
739
740 // Parse body (SDP or other content)
741 let mut body = String::new();
742 if content_length > 0 && i < lines.len() {
743 body = lines[i..].join("\r\n");
744 }
745
746 // Extract method from CSeq
747 let method = cseq.split_whitespace().nth(1).unwrap_or("").to_string();
748
749 Ok(SipResponse {
750 status_code,
751 reason_phrase,
752 method,
753 call_id,
754 from,
755 to,
756 to_tag,
757 cseq,
758 body: if body.is_empty() { None } else { Some(body) },
759 })
760 }
761
762 /// Build SIP request from action data
763 fn build_sip_request(
764 method: &str,
765 data: &serde_json::Value,
766 call_id: &str,
767 from_tag: &str,
768 to_tag: Option<&String>,
769 cseq: u32,
770 local_addr: SocketAddr,
771 ) -> Vec<u8> {
772 let from = data["from"].as_str().unwrap_or("sip:user@localhost");
773 let to = data["to"].as_str().unwrap_or("sip:server@localhost");
774 let request_uri = data["request_uri"]
775 .as_str()
776 .unwrap_or("sip:server@localhost");
777
778 // Map action name to SIP method
779 let sip_method = match method {
780 "sip_register" => "REGISTER",
781 "sip_invite" => "INVITE",
782 "sip_ack" => "ACK",
783 "sip_bye" => "BYE",
784 "sip_options" => "OPTIONS",
785 "sip_cancel" => "CANCEL",
786 _ => "OPTIONS",
787 };
788
789 // Build request line
790 let mut request = format!("{} {} SIP/2.0\r\n", sip_method, request_uri);
791
792 // Add Via header.
793 //
794 // This is our own address, not the literal `127.0.0.1:5060` it used to be. Via is where
795 // an RFC 3261 server sends the response, so a hardcoded loopback address meant every
796 // reply from a real server went to port 5060 on the server's own loopback and this
797 // client never saw it. It worked only against netget's own SIP server, which answers
798 // the datagram's source address instead. `;rport` (RFC 3581) additionally asks the
799 // server to reply to the source address and port it actually observed, which is what
800 // makes this work from behind NAT.
801 request.push_str(&format!(
802 "Via: SIP/2.0/UDP {};rport;branch=z9hG4bK-netget-{}\r\n",
803 local_addr,
804 Self::generate_branch()
805 ));
806
807 // Add From header (with tag)
808 request.push_str(&format!("From: <{}>;tag={}\r\n", from, from_tag));
809
810 // Add To header (with tag if available)
811 if let Some(tag) = to_tag {
812 request.push_str(&format!("To: <{}>;tag={}\r\n", to, tag));
813 } else {
814 request.push_str(&format!("To: <{}>\r\n", to));
815 }
816
817 // Add Call-ID header
818 request.push_str(&format!("Call-ID: {}\r\n", call_id));
819
820 // Add CSeq header
821 request.push_str(&format!("CSeq: {} {}\r\n", cseq, sip_method));
822
823 // Add Contact header (for REGISTER/INVITE)
824 if sip_method == "REGISTER" || sip_method == "INVITE" {
825 // Default Contact is this socket's own address for the same reason as Via: a
826 // server that calls back a hardcoded 127.0.0.1 reaches itself, not us.
827 let default_contact = format!("sip:user@{}", local_addr);
828 let contact = data["contact"].as_str().unwrap_or(&default_contact);
829 request.push_str(&format!("Contact: <{}>\r\n", contact));
830 }
831
832 // Add Expires header (for REGISTER)
833 if sip_method == "REGISTER" {
834 let expires = data["expires"].as_u64().unwrap_or(3600);
835 request.push_str(&format!("Expires: {}\r\n", expires));
836 }
837
838 // Add body (SDP for INVITE)
839 let body = if sip_method == "INVITE" {
840 data["sdp"].as_str()
841 } else {
842 None
843 };
844
845 // Add Content-Length and body
846 if let Some(body_text) = body {
847 request.push_str("Content-Type: application/sdp\r\n");
848 request.push_str(&format!("Content-Length: {}\r\n", body_text.len()));
849 request.push_str("\r\n");
850 request.push_str(body_text);
851 } else {
852 request.push_str("Content-Length: 0\r\n");
853 request.push_str("\r\n");
854 }
855
856 request.into_bytes()
857 }
858
859 /// Build ACK request for INVITE 3-way handshake (RFC 3261)
860 fn build_ack_request(
861 response: &SipResponse,
862 call_id: &str,
863 from_tag: &str,
864 cseq: u32,
865 local_addr: SocketAddr,
866 ) -> Vec<u8> {
867 // Extract URIs from response headers
868 let from_uri = Self::extract_uri(&response.from).unwrap_or("sip:user@localhost");
869 let to_uri = Self::extract_uri(&response.to).unwrap_or("sip:server@localhost");
870
871 // ACK uses same Request-URI as INVITE, which is the To URI
872 let request_uri = to_uri;
873
874 // Build request line
875 let mut request = format!("ACK {} SIP/2.0\r\n", request_uri);
876
877 // Add Via header.
878 //
879 // This is our own address, not the literal `127.0.0.1:5060` it used to be. Via is where
880 // an RFC 3261 server sends the response, so a hardcoded loopback address meant every
881 // reply from a real server went to port 5060 on the server's own loopback and this
882 // client never saw it. It worked only against netget's own SIP server, which answers
883 // the datagram's source address instead. `;rport` (RFC 3581) additionally asks the
884 // server to reply to the source address and port it actually observed, which is what
885 // makes this work from behind NAT.
886 request.push_str(&format!(
887 "Via: SIP/2.0/UDP {};rport;branch=z9hG4bK-netget-{}\r\n",
888 local_addr,
889 Self::generate_branch()
890 ));
891
892 // Add From header (with tag)
893 request.push_str(&format!("From: <{}>;tag={}\r\n", from_uri, from_tag));
894
895 // Add To header (with tag from response - ACK must include To tag)
896 if let Some(to_tag) = &response.to_tag {
897 request.push_str(&format!("To: <{}>;tag={}\r\n", to_uri, to_tag));
898 } else {
899 request.push_str(&format!("To: <{}>\r\n", to_uri));
900 }
901
902 // Add Call-ID header
903 request.push_str(&format!("Call-ID: {}\r\n", call_id));
904
905 // Add CSeq header (same number as INVITE, but ACK method)
906 request.push_str(&format!("CSeq: {} ACK\r\n", cseq));
907
908 // ACK has no body
909 request.push_str("Content-Length: 0\r\n");
910 request.push_str("\r\n");
911
912 request.into_bytes()
913 }
914
915 /// Extract SIP URI from header value (removes display name and parameters)
916 fn extract_uri(header_value: &str) -> Option<&str> {
917 // Handle format: "Display Name" <sip:user@host>;tag=xyz
918 // or: sip:user@host;tag=xyz
919 // or: <sip:user@host>
920
921 let trimmed = header_value.trim();
922
923 // Check for angle brackets.
924 //
925 // The closing bracket is searched for *after* the opening one. Searching the whole
926 // string for '>' independently panicked whenever a '>' appeared first — which a peer
927 // can arrange with a display name such as `"a>b" <sip:x@y>` — because `&s[start+1..end]`
928 // with `start > end` is a slice whose start is past its end. Remotely triggerable, in
929 // the read loop, inside a `tokio::spawn` that swallows the panic.
930 if let Some(start) = trimmed.find('<') {
931 if let Some(offset) = trimmed[start + 1..].find('>') {
932 return Some(&trimmed[start + 1..start + 1 + offset]);
933 }
934 }
935
936 // No angle brackets, extract until semicolon or end
937 if let Some(semicolon) = trimmed.find(';') {
938 return Some(&trimmed[..semicolon]);
939 }
940
941 Some(trimmed)
942 }
943
944 /// Generate a random Call-ID
945 fn generate_call_id() -> String {
946 use rand::Rng;
947 let random: u64 = rand::thread_rng().gen();
948 format!("{}@netget-client", random)
949 }
950
951 /// Generate a random tag
952 fn generate_tag() -> String {
953 use rand::Rng;
954 let tag: u32 = rand::thread_rng().gen();
955 format!("{:x}", tag)
956 }
957
958 /// Generate a random branch parameter
959 fn generate_branch() -> String {
960 use rand::Rng;
961 let branch: u32 = rand::thread_rng().gen();
962 format!("{:x}", branch)
963 }
964}
965
966/// Parsed SIP response
967#[derive(Debug, Clone)]
968struct SipResponse {
969 status_code: u16,
970 reason_phrase: String,
971 method: String, // Extracted from CSeq
972 call_id: String,
973 from: String,
974 to: String,
975 to_tag: Option<String>,
976 /// Full CSeq header value (kept for debugging and future use)
977 #[allow(dead_code)]
978 cseq: String,
979 body: Option<String>,
980}