1pub mod actions;
37pub mod wire;
38
39use anyhow::{anyhow, Result};
40use std::net::{Ipv4Addr, SocketAddr};
41use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
42use std::sync::Arc;
43use tokio::io::{AsyncReadExt, AsyncWriteExt};
44use tokio::sync::{mpsc, Notify};
45use tracing::{debug, error, info, trace, warn};
46
47use crate::llm::action_helper::call_llm;
48use crate::llm::actions::protocol_trait::ActionResult;
49use crate::llm::ollama_client::OllamaClient;
50use crate::logging::emit::Log;
51use crate::protocol::Event;
52use crate::server::BgpProtocol;
53use crate::state::app_state::AppState;
54use crate::state::server::BgpSessionState;
55use crate::utils::WireFailure;
56use actions::{
57 BGP_ESTABLISHED_EVENT, BGP_MESSAGE_INTENT, BGP_NOTIFICATION_EVENT, BGP_OPEN_EVENT,
58 BGP_UPDATE_EVENT,
59};
60use netgauze_bgp_pkt::{capabilities::BgpCapability, BgpMessage};
61
62const DEFAULT_HOLD_TIME: u16 = 180;
65
66const MIN_HOLD_TIME: u16 = 3;
68
69const OPEN_HOLD_TIME: std::time::Duration = std::time::Duration::from_secs(240);
79
80const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
96
97static CONNECTION_CAP_REFUSAL: std::sync::LazyLock<Vec<u8>> = std::sync::LazyLock::new(|| {
121 wire::encode_notification(wire::ERR_CEASE, wire::SUB_CEASE_OUT_OF_RESOURCES, &[])
122 .expect("a NOTIFICATION with no data is 21 bytes and cannot exceed the BGP maximum")
123});
124
125pub struct BgpServer;
127
128impl BgpServer {
129 pub async fn spawn_with_llm_actions(
134 listen_addr: SocketAddr,
135 llm_client: OllamaClient,
136 app_state: Arc<AppState>,
137 status_tx: mpsc::UnboundedSender<String>,
138 server_id: crate::state::ServerId,
139 startup_params: Option<crate::protocol::StartupParams>,
140 ) -> Result<SocketAddr> {
141 let listener =
142 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
143 let local_addr = listener.local_addr()?;
144
145 let config = BgpConfig::from_params(startup_params.as_ref())?;
146 Log::new(Some(&status_tx)).info(format!(
147 "BGP server listening on {} as AS{} router-id {} hold {}s",
148 local_addr, config.local_as, config.router_id, config.hold_time
149 ));
150
151 let protocol = Arc::new(BgpProtocol::new());
152
153 let task_registrar = app_state.clone();
154 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
155 let accept_handle = tokio::spawn(async move {
156 loop {
157 match crate::server::accept_bounded::accept_bounded(
158 &listener,
159 &limiter,
160 &CONNECTION_CAP_REFUSAL,
161 "BGP",
162 Some(&status_tx),
163 )
164 .await
165 {
166 Ok((stream, remote_addr, permit)) => {
167 let connection_id = crate::server::connection::ConnectionId::new(
168 app_state.get_next_unified_id().await,
169 );
170 Log::new(Some(&status_tx)).info(format!(
171 "BGP connection {} from {}",
172 connection_id, remote_addr
173 ));
174
175 register_connection(&app_state, server_id, connection_id, remote_addr)
176 .await;
177 let _ = status_tx.send("__UPDATE_UI__".to_string());
178
179 let llm_clone = llm_client.clone();
180 let state_clone = app_state.clone();
181 let status_clone = status_tx.clone();
182 let protocol_clone = protocol.clone();
183 let config_clone = config.clone();
184
185 let session_registrar = app_state.clone();
186 let session_handle = tokio::spawn(async move {
187 let _permit = permit;
194 if let Err(e) = run_session(
195 stream,
196 connection_id,
197 server_id,
198 remote_addr,
199 llm_clone,
200 state_clone.clone(),
201 status_clone.clone(),
202 protocol_clone,
203 config_clone,
204 )
205 .await
206 {
207 Log::new(Some(&status_clone))
208 .error(format!("BGP session error: {}", e));
209 }
210
211 state_clone
212 .close_connection_on_server(server_id, connection_id)
213 .await;
214 Log::new(Some(&status_clone))
215 .info(format!("BGP connection {} closed", connection_id));
216 let _ = status_clone.send("__UPDATE_UI__".to_string());
217 });
218 session_registrar
225 .register_server_task(server_id, session_handle)
226 .await;
227 }
228 Err(e) => {
229 Log::new(Some(&status_tx))
230 .error(format!("Failed to accept BGP connection: {}", e));
231 break;
232 }
233 }
234 }
235 });
236
237 task_registrar
239 .register_server_task(server_id, accept_handle)
240 .await;
241
242 Ok(local_addr)
243 }
244}
245
246#[derive(Debug, Clone)]
248pub struct BgpConfig {
249 pub local_as: u32,
250 pub router_id: Ipv4Addr,
251 pub hold_time: u16,
252}
253
254impl BgpConfig {
255 fn from_params(params: Option<&crate::protocol::StartupParams>) -> Result<Self> {
262 let Some(params) = params else {
263 return Ok(Self {
264 local_as: 65000,
265 router_id: Ipv4Addr::new(192, 168, 1, 1),
266 hold_time: DEFAULT_HOLD_TIME,
267 });
268 };
269
270 let local_as = params.get_optional_u32("as_number")?.unwrap_or(65000);
271 if local_as == 0 {
272 return Err(anyhow!("as_number must be between 1 and 4294967295"));
273 }
274
275 let router_id = match params.get_optional_string("router_id")? {
276 Some(s) => s.parse::<Ipv4Addr>().map_err(|_| {
277 anyhow!("router_id must be an IPv4 address in dotted-quad form, got {s:?}")
278 })?,
279 None => Ipv4Addr::new(192, 168, 1, 1),
280 };
281 if router_id.is_unspecified() {
282 return Err(anyhow!(
283 "router_id must not be 0.0.0.0 (RFC 4271 requires a valid BGP Identifier)"
284 ));
285 }
286
287 let hold_time = match params.get_optional_u64("hold_time")? {
288 Some(v) => {
289 let v = u16::try_from(v)
290 .map_err(|_| anyhow!("hold_time must be between 0 and 65535, got {v}"))?;
291 if v != 0 && v < MIN_HOLD_TIME {
292 return Err(anyhow!(
293 "hold_time must be 0 (timers disabled) or at least {MIN_HOLD_TIME} seconds"
294 ));
295 }
296 v
297 }
298 None => DEFAULT_HOLD_TIME,
299 };
300
301 Ok(Self {
302 local_as,
303 router_id,
304 hold_time,
305 })
306 }
307}
308
309async fn register_connection(
310 app_state: &AppState,
311 server_id: crate::state::ServerId,
312 connection_id: crate::server::connection::ConnectionId,
313 remote_addr: SocketAddr,
314) {
315 use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
316 let now = crate::utils::clock::Instant::now();
317 app_state
318 .add_connection_to_server(
319 server_id,
320 ConnectionState {
321 id: connection_id,
322 remote_addr,
323 local_addr: remote_addr,
324 bytes_sent: 0,
325 bytes_received: 0,
326 packets_sent: 0,
327 packets_received: 0,
328 last_activity: now,
329 status: ConnectionStatus::Active,
330 status_changed_at: now,
331 protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
332 "bgp_state": "Connect",
333 })),
334 },
335 )
336 .await;
337}
338
339struct BgpSession {
341 connection_id: crate::server::connection::ConnectionId,
342 server_id: crate::state::ServerId,
343 remote_addr: SocketAddr,
344 llm_client: OllamaClient,
345 app_state: Arc<AppState>,
346 status_tx: mpsc::UnboundedSender<String>,
347 protocol: Arc<BgpProtocol>,
348 config: BgpConfig,
349
350 state: BgpSessionState,
351 peer_as: Option<u32>,
353 peer_router_id: Option<Ipv4Addr>,
354 peer_asn4: bool,
357 hold_time: u16,
359
360 out_tx: mpsc::UnboundedSender<Vec<u8>>,
361 peer_asn4_shared: Arc<AtomicBool>,
364 last_received: Arc<AtomicU64>,
366 started: crate::utils::clock::Instant,
367}
368
369enum Incoming {
371 Message(BgpMessage),
372 HeaderError(wire::HeaderError),
374 DecodeError(wire::DecodeError, u8),
377 Eof,
378}
379
380#[allow(clippy::too_many_arguments)]
381async fn run_session(
382 stream: tokio::net::TcpStream,
383 connection_id: crate::server::connection::ConnectionId,
384 server_id: crate::state::ServerId,
385 remote_addr: SocketAddr,
386 llm_client: OllamaClient,
387 app_state: Arc<AppState>,
388 status_tx: mpsc::UnboundedSender<String>,
389 protocol: Arc<BgpProtocol>,
390 config: BgpConfig,
391) -> Result<()> {
392 let (mut reader, mut writer) = tokio::io::split(stream);
395 let (out_tx, mut out_rx) = mpsc::unbounded_channel::<Vec<u8>>();
396
397 let stats_state = app_state.clone();
401 let writer_task = tokio::spawn(async move {
402 while let Some(bytes) = out_rx.recv().await {
403 if writer.write_all(&bytes).await.is_err() {
404 break;
405 }
406 if writer.flush().await.is_err() {
407 break;
408 }
409 stats_state
410 .update_connection_stats(
411 server_id,
412 connection_id,
413 None,
414 Some(bytes.len() as u64),
415 None,
416 Some(1),
417 )
418 .await;
419 }
420 let _ = writer.shutdown().await;
421 });
422
423 let shutdown = Arc::new(Shutdown::default());
424 let peer_asn4_shared = Arc::new(AtomicBool::new(false));
425
426 let peer_rx = crate::server::peer_support::register_peer_channel(
433 &app_state,
434 server_id,
435 connection_id.as_u32(),
436 )
437 .await;
438 let peer_task = spawn_peer_command_task(
439 peer_rx,
440 protocol.clone(),
441 app_state.clone(),
442 server_id,
443 connection_id,
444 out_tx.clone(),
445 peer_asn4_shared.clone(),
446 shutdown.clone(),
447 status_tx.clone(),
448 )
449 .await;
450
451 let mut session = BgpSession {
452 connection_id,
453 server_id,
454 remote_addr,
455 llm_client,
456 app_state,
457 status_tx,
458 protocol,
459 config,
460 state: BgpSessionState::Connect,
461 peer_as: None,
462 peer_router_id: None,
463 peer_asn4: false,
464 hold_time: 0,
465 out_tx,
466 peer_asn4_shared,
467 last_received: Arc::new(AtomicU64::new(0)),
468 started: crate::utils::clock::Instant::now(),
469 };
470
471 let mut timer_task: Option<tokio::task::AbortHandle> = None;
472 let result = session.run(&mut reader, &shutdown, &mut timer_task).await;
473
474 session
478 .app_state
479 .remove_peer_handle(server_id, connection_id.as_u32())
480 .await;
481 peer_task.abort();
482
483 if let Some(handle) = timer_task {
486 handle.abort();
487 }
488 drop(session);
489 let _ = writer_task.await;
490
491 result
492}
493
494impl BgpSession {
495 async fn run(
496 &mut self,
497 reader: &mut tokio::io::ReadHalf<tokio::net::TcpStream>,
498 shutdown: &Arc<Shutdown>,
499 timer_task: &mut Option<tokio::task::AbortHandle>,
500 ) -> Result<()> {
501 debug!("BGP session {} in Connect state", self.connection_id);
502
503 loop {
504 if shutdown.is_set() {
505 break;
506 }
507 let open_deadline =
512 matches!(self.state, BgpSessionState::Connect).then_some(OPEN_HOLD_TIME);
513
514 let (incoming, bytes_in) = tokio::select! {
515 biased;
516 _ = shutdown.notify.notified() => break,
517 result = with_deadline(open_deadline, read_message(reader, self.peer_asn4)) => {
518 match result {
519 Ok(incoming) => incoming,
520 Err(DeadlineExpired) => {
521 Log::new(Some(&self.status_tx)).warn(format!(
522 "BGP peer {} sent no OPEN within {}s, closing (RFC 4271 8.2.2)",
523 self.remote_addr,
524 OPEN_HOLD_TIME.as_secs()
525 ));
526 self.notify(wire::ERR_HOLD_TIMER_EXPIRED, 0, &[]);
527 break;
528 }
529 }
530 }
531 };
532 if bytes_in > 0 {
533 self.app_state
534 .update_connection_stats(
535 self.server_id,
536 self.connection_id,
537 Some(bytes_in as u64),
538 None,
539 Some(1),
540 None,
541 )
542 .await;
543 }
544
545 match incoming {
546 Incoming::Eof => {
547 debug!("BGP connection {} closed by peer", self.connection_id);
548 break;
549 }
550 Incoming::HeaderError(e) => {
551 Log::new(Some(&self.status_tx)).warn(format!(
552 "BGP header rejected from {}: {:?}",
553 self.remote_addr, e
554 ));
555 let (code, sub) = e.notify_code();
556 self.notify(code, sub, &e.notify_data());
557 break;
558 }
559 Incoming::DecodeError(e, msg_type) => {
560 Log::new(Some(&self.status_tx)).warn(format!(
561 "BGP message from {} failed to parse: {}",
562 self.remote_addr, e
563 ));
564 if msg_type != wire::MSG_NOTIFICATION {
565 let (code, sub) = e.notify;
566 self.notify(code, sub, &[]);
567 }
568 break;
569 }
570 Incoming::Message(msg) => {
571 self.last_received
572 .store(self.started.elapsed().as_secs(), Ordering::Relaxed);
573 if !self.dispatch(msg, shutdown, timer_task).await? {
574 break;
575 }
576 }
577 }
578 }
579
580 Ok(())
581 }
582
583 async fn dispatch(
585 &mut self,
586 msg: BgpMessage,
587 shutdown: &Arc<Shutdown>,
588 timer_task: &mut Option<tokio::task::AbortHandle>,
589 ) -> Result<bool> {
590 match (self.state.clone(), msg) {
591 (BgpSessionState::Connect, BgpMessage::Open(open)) => {
592 self.on_open(open, shutdown, timer_task).await
593 }
594 (BgpSessionState::Connect, BgpMessage::Notification(n)) => {
596 self.on_notification(n).await;
597 Ok(false)
598 }
599 (BgpSessionState::Connect, _) => {
602 self.notify(wire::ERR_FSM, wire::SUB_FSM_OPENSENT, &[]);
603 Ok(false)
604 }
605
606 (BgpSessionState::OpenConfirm, BgpMessage::KeepAlive) => {
607 self.on_established().await?;
608 Ok(true)
609 }
610 (BgpSessionState::OpenConfirm, BgpMessage::Notification(n)) => {
611 self.on_notification(n).await;
612 Ok(false)
613 }
614 (BgpSessionState::OpenConfirm, _) => {
615 self.notify(wire::ERR_FSM, wire::SUB_FSM_OPENCONFIRM, &[]);
616 Ok(false)
617 }
618
619 (BgpSessionState::Established, BgpMessage::KeepAlive) => {
620 trace!("BGP KEEPALIVE from {}", self.remote_addr);
625 Ok(true)
626 }
627 (BgpSessionState::Established, BgpMessage::Update(update)) => {
628 self.on_update(update).await?;
629 Ok(true)
630 }
631 (BgpSessionState::Established, BgpMessage::Notification(n)) => {
632 self.on_notification(n).await;
633 Ok(false)
634 }
635 (BgpSessionState::Established, BgpMessage::RouteRefresh(_)) => {
636 warn!(
642 "BGP ROUTE-REFRESH from {} ignored: no RIB to re-advertise",
643 self.remote_addr
644 );
645 Ok(true)
646 }
647 (BgpSessionState::Established, BgpMessage::Open(_)) => {
648 self.notify(wire::ERR_FSM, wire::SUB_FSM_ESTABLISHED, &[]);
649 Ok(false)
650 }
651
652 (_, BgpMessage::Notification(n)) => {
653 self.on_notification(n).await;
654 Ok(false)
655 }
656 (state, msg) => {
657 warn!(
658 "BGP {:?} message in unexpected state {:?}",
659 msg.get_type(),
660 state
661 );
662 self.notify(wire::ERR_FSM, 0, &[]);
663 Ok(false)
664 }
665 }
666 }
667
668 async fn on_open(
670 &mut self,
671 open: netgauze_bgp_pkt::open::BgpOpenMessage,
672 shutdown: &Arc<Shutdown>,
673 timer_task: &mut Option<tokio::task::AbortHandle>,
674 ) -> Result<bool> {
675 let peer_as = open.my_asn4();
678 if peer_as == 0 {
679 Log::new(Some(&self.status_tx)).warn("BGP peer advertised AS 0, rejecting");
680 self.notify(wire::ERR_OPEN, wire::SUB_BAD_PEER_AS, &[]);
681 return Ok(false);
682 }
683 let peer_hold = open.hold_time();
684 if peer_hold != 0 && peer_hold < MIN_HOLD_TIME {
685 self.notify(wire::ERR_OPEN, wire::SUB_UNACCEPTABLE_HOLD_TIME, &[]);
686 return Ok(false);
687 }
688
689 self.peer_as = Some(peer_as);
690 self.peer_router_id = Some(open.bgp_id());
691 self.peer_asn4 = open
692 .capabilities()
693 .into_iter()
694 .any(|c| matches!(c, BgpCapability::FourOctetAs(_)));
695 self.peer_asn4_shared
696 .store(self.peer_asn4, Ordering::Relaxed);
697
698 self.hold_time = negotiate_hold(self.config.hold_time, peer_hold);
702
703 Log::new(Some(&self.status_tx)).info(format!(
704 "BGP OPEN from AS{} router-id {} hold {}s (asn4={}), negotiated hold {}s",
705 peer_as,
706 open.bgp_id(),
707 peer_hold,
708 self.peer_asn4,
709 self.hold_time
710 ));
711
712 let event = Event {
713 event_type: &BGP_OPEN_EVENT,
714 data: serde_json::json!({
715 "connection_id": self.connection_id.to_string(),
716 "peer_as": peer_as,
717 "peer_router_id": open.bgp_id().to_string(),
718 "peer_hold_time": peer_hold,
719 "peer_supports_four_octet_as": self.peer_asn4,
720 "peer_capabilities": wire::capabilities_to_json(&open),
721 "negotiated_hold_time": self.hold_time,
722 "local_as": self.config.local_as,
723 "local_router_id": self.config.router_id.to_string(),
724 "remote_addr": self.remote_addr.to_string(),
725 }),
726 };
727
728 let outcome = self.call_llm_and_send(&event).await;
729
730 match outcome {
731 SendOutcome::Refused => {
736 info!(
737 "BGP bgp_open decision=model_reject peer={} AS{}",
738 self.remote_addr, peer_as
739 );
740 Ok(false)
741 }
742 SendOutcome::Failed(failure) => {
749 Log::new(Some(&self.status_tx)).warn(format!(
750 "BGP refusing to peer with {} AS{}: peering policy could not be evaluated \
751 (decision=fail_closed_llm_error, {})",
752 self.remote_addr,
753 peer_as,
754 if failure.is_overloaded() {
755 "resource"
756 } else {
757 "unavailable"
758 }
759 ));
760 self.notify(wire::ERR_CEASE, SendOutcome::cease_subcode(failure), &[]);
761 Ok(false)
762 }
763 SendOutcome::SentOpen { hold_time } => {
764 self.hold_time = negotiate_hold(hold_time, peer_hold);
770 self.after_open_sent(shutdown, timer_task).await;
771 Ok(true)
772 }
773 SendOutcome::Nothing => {
774 Log::new(Some(&self.status_tx)).warn(format!(
782 "BGP no OPEN from handler for {} (decision=model_silent), sending \
783 configured OPEN AS{}",
784 self.remote_addr, self.config.local_as
785 ));
786 let bytes = wire::encode(wire::build_open(
787 self.config.local_as,
788 self.config.hold_time,
789 self.config.router_id,
790 ))?;
791 self.send(bytes);
792 self.hold_time = negotiate_hold(self.config.hold_time, peer_hold);
793 self.after_open_sent(shutdown, timer_task).await;
794 Ok(true)
795 }
796 }
797 }
798
799 async fn after_open_sent(
803 &mut self,
804 shutdown: &Arc<Shutdown>,
805 timer_task: &mut Option<tokio::task::AbortHandle>,
806 ) {
807 self.send(wire::encode_keepalive());
808 self.state = BgpSessionState::OpenConfirm;
809 debug!("BGP session {} -> OpenConfirm", self.connection_id);
810
811 if self.hold_time > 0 && timer_task.is_none() {
812 *timer_task = Some(
813 spawn_timers(
814 self.hold_time,
815 self.out_tx.clone(),
816 self.last_received.clone(),
817 self.started,
818 shutdown.clone(),
819 self.status_tx.clone(),
820 self.connection_id,
821 self.app_state.clone(),
822 self.server_id,
823 )
824 .await,
825 );
826 }
827 }
828
829 async fn on_established(&mut self) -> Result<()> {
830 self.state = BgpSessionState::Established;
831 let peer_as = self.peer_as.unwrap_or(0);
832 Log::new(Some(&self.status_tx)).info(format!(
833 "BGP session {} established with AS{}",
834 self.connection_id, peer_as
835 ));
836 self.set_connection_state("Established").await;
837
838 let event = Event {
841 event_type: &BGP_ESTABLISHED_EVENT,
842 data: serde_json::json!({
843 "connection_id": self.connection_id.to_string(),
844 "peer_as": peer_as,
845 "peer_router_id": self.peer_router_id.map(|r| r.to_string()),
846 "peer_supports_four_octet_as": self.peer_asn4,
847 "negotiated_hold_time": self.hold_time,
848 "local_as": self.config.local_as,
849 "local_router_id": self.config.router_id.to_string(),
850 "remote_addr": self.remote_addr.to_string(),
851 }),
852 };
853 let outcome = self.call_llm_and_send(&event).await;
854 self.log_advertisement_outcome("bgp_established", outcome);
855 Ok(())
856 }
857
858 fn log_advertisement_outcome(&self, event_id: &str, outcome: SendOutcome) {
868 match outcome {
869 SendOutcome::Failed(failure) => {
870 Log::new(Some(&self.status_tx)).warn(format!(
872 "BGP {} advertised nothing to {} (decision=fail_closed_llm_error, {})",
873 event_id,
874 self.remote_addr,
875 if failure.is_overloaded() {
876 "resource"
877 } else {
878 "unavailable"
879 }
880 ));
881 }
882 SendOutcome::Nothing => debug!(
883 "BGP {} decision=model_silent peer={}: nothing advertised",
884 event_id, self.remote_addr
885 ),
886 SendOutcome::Refused => info!(
887 "BGP {} decision=model_reject peer={}: NOTIFICATION sent",
888 event_id, self.remote_addr
889 ),
890 SendOutcome::SentOpen { .. } => debug!(
891 "BGP {} peer={}: handler answered",
892 event_id, self.remote_addr
893 ),
894 }
895 }
896
897 async fn on_update(
898 &mut self,
899 update: netgauze_bgp_pkt::update::BgpUpdateMessage,
900 ) -> Result<()> {
901 let parsed = wire::update_to_json(&update);
902 info!(
903 "BGP UPDATE from AS{:?}: {} withdrawn, {} announced",
904 self.peer_as,
905 update.withdraw_routes().len(),
906 update.nlri().len()
907 );
908
909 let mut data = parsed;
910 data["connection_id"] = serde_json::json!(self.connection_id.to_string());
911 data["peer_as"] = serde_json::json!(self.peer_as);
912
913 let event = Event {
914 event_type: &BGP_UPDATE_EVENT,
915 data,
916 };
917 let outcome = self.call_llm_and_send(&event).await;
918 self.log_advertisement_outcome("bgp_update", outcome);
919 Ok(())
920 }
921
922 async fn on_notification(&mut self, n: netgauze_bgp_pkt::notification::BgpNotificationMessage) {
923 let (code, subcode) = notification_codes(&n);
924 Log::new(Some(&self.status_tx)).warn(format!(
925 "BGP NOTIFICATION from {}: {} / {}",
926 self.remote_addr,
927 wire::error_name(code),
928 wire::error_subcode_name(code, subcode)
929 ));
930
931 let event = Event {
932 event_type: &BGP_NOTIFICATION_EVENT,
933 data: serde_json::json!({
934 "connection_id": self.connection_id.to_string(),
935 "peer_as": self.peer_as,
936 "error_code": code,
937 "error_name": wire::error_name(code),
938 "error_subcode": subcode,
939 "error_subcode_name": wire::error_subcode_name(code, subcode),
940 }),
941 };
942 if operator_wants_dynamic(&self.app_state, self.server_id, &event.event_type.id).await {
948 if let Err(e) = call_llm(
952 &self.llm_client,
953 &self.app_state,
954 self.server_id,
955 Some(self.connection_id),
956 &event,
957 &*self.protocol,
958 )
959 .await
960 {
961 error!(
962 "BGP bgp_notification decision=fail_closed_llm_error category={:?} peer={}: \
963 {:#}",
964 WireFailure::classify(&e),
965 self.remote_addr,
966 e
967 );
968 }
969 }
970 }
971
972 async fn call_llm_and_send(&mut self, event: &Event) -> SendOutcome {
974 if !operator_wants_dynamic(&self.app_state, self.server_id, &event.event_type.id).await {
983 debug!(
984 "BGP {} handled statically (no operator policy configured): no LLM call",
985 event.event_type.id
986 );
987 return SendOutcome::Nothing;
988 }
989
990 let result = match call_llm(
991 &self.llm_client,
992 &self.app_state,
993 self.server_id,
994 Some(self.connection_id),
995 event,
996 &*self.protocol,
997 )
998 .await
999 {
1000 Ok(result) => result,
1001 Err(e) => {
1002 let failure = WireFailure::classify(&e);
1006 error!(
1007 "BGP {} decision=fail_closed_llm_error category={:?} peer={}: {:#}",
1008 event.event_type.id, failure, self.remote_addr, e
1009 );
1010 Log::new(Some(&self.status_tx)).warn(format!(
1011 "BGP handler for {} could not be run (decision=fail_closed_llm_error): {}",
1012 event.event_type.id, e
1013 ));
1014 return SendOutcome::Failed(failure);
1015 }
1016 };
1017
1018 let mut outcome = SendOutcome::Nothing;
1019 let mut queue: Vec<ActionResult> = result.protocol_results;
1020 while let Some(item) = queue.pop() {
1021 match item {
1022 ActionResult::Multiple(inner) => queue.extend(inner),
1023 ActionResult::Custom { name, data } if name == BGP_MESSAGE_INTENT => {
1024 let kind = data.get("kind").and_then(|v| v.as_str()).unwrap_or("");
1025 match wire::encode_intent(&data, self.peer_asn4) {
1026 Ok(bytes) => {
1027 self.send(bytes);
1028 outcome = match kind {
1029 "open" => SendOutcome::SentOpen {
1030 hold_time: data
1031 .get("hold_time")
1032 .and_then(|v| v.as_u64())
1033 .unwrap_or(0)
1034 as u16,
1035 },
1036 "notification" => SendOutcome::Refused,
1037 _ => match outcome {
1038 SendOutcome::Nothing => SendOutcome::Nothing,
1039 other => other,
1040 },
1041 };
1042 }
1043 Err(e) => {
1044 Log::new(Some(&self.status_tx))
1047 .warn(format!("BGP refused to encode {} action: {}", kind, e));
1048 }
1049 }
1050 }
1051 ActionResult::Output(bytes) => self.send(bytes),
1054 _ => {}
1055 }
1056 }
1057 outcome
1058 }
1059
1060 fn send(&self, bytes: Vec<u8>) {
1061 if self.out_tx.send(bytes).is_err() {
1062 debug!("BGP write channel closed on {}", self.connection_id);
1063 }
1064 }
1065
1066 fn notify(&self, code: u8, subcode: u8, data: &[u8]) {
1067 match wire::encode_notification(code, subcode, data) {
1068 Ok(bytes) => {
1069 Log::new(Some(&self.status_tx)).warn(format!(
1070 "BGP NOTIFICATION {} / {} to {}",
1071 wire::error_name(code),
1072 wire::error_subcode_name(code, subcode),
1073 self.remote_addr
1074 ));
1075 self.send(bytes);
1076 }
1077 Err(e) => error!("BGP could not encode NOTIFICATION {code}/{subcode}: {e}"),
1078 }
1079 }
1080
1081 async fn set_connection_state(&self, state: &str) {
1082 let server_id = self.server_id;
1083 let conn_id = self.connection_id;
1084 let value = serde_json::json!({ "bgp_state": state });
1085 self.app_state
1086 .with_server_mut(server_id, |server| {
1087 if let Some(conn) = server.connections.get_mut(&conn_id) {
1088 conn.protocol_info =
1089 crate::state::server::ProtocolConnectionInfo::new(value.clone());
1090 }
1091 })
1092 .await;
1093 }
1094}
1095
1096#[derive(Default)]
1103struct Shutdown {
1104 flag: AtomicBool,
1105 notify: Notify,
1106}
1107
1108impl Shutdown {
1109 fn set(&self) {
1110 self.flag.store(true, Ordering::SeqCst);
1111 self.notify.notify_waiters();
1112 }
1113
1114 fn is_set(&self) -> bool {
1115 self.flag.load(Ordering::SeqCst)
1116 }
1117}
1118
1119async fn operator_wants_dynamic(
1124 state: &AppState,
1125 server_id: crate::state::ServerId,
1126 event_id: &str,
1127) -> bool {
1128 state
1129 .with_server_mut(server_id, |server| {
1130 let has_instruction = !server.instruction.trim().is_empty();
1131 let has_handler = server
1132 .event_handler_config
1133 .as_ref()
1134 .map(|c| c.find_handler(event_id).is_some())
1135 .unwrap_or(false);
1136 has_instruction || has_handler
1137 })
1138 .await
1139 .unwrap_or(false)
1140}
1141
1142fn negotiate_hold(ours: u16, theirs: u16) -> u16 {
1145 if ours == 0 || theirs == 0 {
1146 0
1147 } else {
1148 ours.min(theirs)
1149 }
1150}
1151
1152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1154enum SendOutcome {
1155 SentOpen { hold_time: u16 },
1157 Refused,
1159 Nothing,
1162 Failed(WireFailure),
1166}
1167
1168impl SendOutcome {
1169 fn cease_subcode(failure: WireFailure) -> u8 {
1173 match failure {
1174 WireFailure::Overloaded => wire::SUB_CEASE_OUT_OF_RESOURCES,
1175 WireFailure::Unavailable => wire::SUB_CEASE_CONNECTION_REJECTED,
1176 }
1177 }
1178}
1179
1180#[allow(clippy::too_many_arguments)]
1186async fn spawn_timers(
1187 hold_time: u16,
1188 out_tx: mpsc::UnboundedSender<Vec<u8>>,
1189 last_received: Arc<AtomicU64>,
1190 started: crate::utils::clock::Instant,
1191 shutdown: Arc<Shutdown>,
1192 status_tx: mpsc::UnboundedSender<String>,
1193 connection_id: crate::server::connection::ConnectionId,
1194 app_state: Arc<AppState>,
1195 server_id: crate::state::ServerId,
1196) -> tokio::task::AbortHandle {
1197 let interval = std::time::Duration::from_secs(u64::from(hold_time).div_ceil(3).max(1));
1199 let task_owner = app_state.clone();
1204 task_owner
1205 .spawn_server_task(server_id, async move {
1206 let keepalive = wire::encode_keepalive();
1207 loop {
1208 tokio::time::sleep(interval).await;
1209
1210 let elapsed = started.elapsed().as_secs();
1211 let last = last_received.load(Ordering::Relaxed);
1212 if elapsed.saturating_sub(last) >= u64::from(hold_time) {
1213 Log::new(Some(&status_tx)).warn(format!(
1214 "BGP hold timer expired on {} after {}s of silence",
1215 connection_id,
1216 elapsed.saturating_sub(last)
1217 ));
1218 if let Ok(bytes) =
1219 wire::encode_notification(wire::ERR_HOLD_TIMER_EXPIRED, 0, &[])
1220 {
1221 let _ = out_tx.send(bytes);
1222 }
1223 shutdown.set();
1224 return;
1225 }
1226
1227 if out_tx.send(keepalive.clone()).is_err() {
1228 return;
1229 }
1230 trace!("BGP KEEPALIVE sent on {}", connection_id);
1231 }
1232 })
1233 .await
1234}
1235
1236struct DeadlineExpired;
1238
1239async fn with_deadline<F: std::future::Future>(
1245 limit: Option<std::time::Duration>,
1246 fut: F,
1247) -> std::result::Result<F::Output, DeadlineExpired> {
1248 match limit {
1249 Some(limit) => tokio::time::timeout(limit, fut)
1250 .await
1251 .map_err(|_| DeadlineExpired),
1252 None => Ok(fut.await),
1253 }
1254}
1255
1256async fn read_message(
1263 reader: &mut tokio::io::ReadHalf<tokio::net::TcpStream>,
1264 asn4: bool,
1265) -> (Incoming, usize) {
1266 let mut header = [0u8; wire::BGP_HEADER_LEN];
1267 match reader.read_exact(&mut header).await {
1268 Ok(_) => {}
1269 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return (Incoming::Eof, 0),
1270 Err(e) => {
1271 debug!("BGP read error: {e}");
1272 return (Incoming::Eof, 0);
1273 }
1274 }
1275
1276 let (len, msg_type) = match wire::parse_header(&header) {
1277 Ok(v) => v,
1278 Err(e) => return (Incoming::HeaderError(e), wire::BGP_HEADER_LEN),
1279 };
1280
1281 let mut full = vec![0u8; len];
1282 full[..wire::BGP_HEADER_LEN].copy_from_slice(&header);
1283 if len > wire::BGP_HEADER_LEN {
1284 if let Err(e) = reader.read_exact(&mut full[wire::BGP_HEADER_LEN..]).await {
1285 debug!("BGP truncated message body: {e}");
1286 return (Incoming::Eof, wire::BGP_HEADER_LEN);
1287 }
1288 }
1289
1290 let incoming = match wire::decode(&full, asn4) {
1291 Ok(msg) => Incoming::Message(msg),
1292 Err(e) => Incoming::DecodeError(e, msg_type),
1293 };
1294 (incoming, len)
1295}
1296
1297#[allow(clippy::too_many_arguments)]
1309async fn spawn_peer_command_task(
1310 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
1311 protocol: Arc<BgpProtocol>,
1312 app_state: Arc<AppState>,
1313 server_id: crate::state::ServerId,
1314 connection_id: crate::server::connection::ConnectionId,
1315 out_tx: mpsc::UnboundedSender<Vec<u8>>,
1316 peer_asn4: Arc<AtomicBool>,
1317 shutdown: Arc<Shutdown>,
1318 status_tx: mpsc::UnboundedSender<String>,
1319) -> tokio::task::AbortHandle {
1320 use crate::llm::actions::protocol_trait::Protocol;
1321 use crate::state::client_handles::ClientSendOutcome;
1322 use crate::state::AccessLogOwner;
1323
1324 let task_owner = app_state.clone();
1326 task_owner
1327 .spawn_server_task(server_id, async move {
1328 while let Some(command) = command_rx.recv().await {
1329 let action = command.action.clone();
1330 let outcome = execute_injected_action(
1331 &action,
1332 protocol.as_ref(),
1333 &app_state,
1334 server_id,
1335 &out_tx,
1336 peer_asn4.load(Ordering::Relaxed),
1337 &shutdown,
1338 )
1339 .await;
1340
1341 let outcome_json = match &outcome {
1342 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
1343 Err(e) => serde_json::json!({"error": e.to_string()}),
1344 };
1345 app_state
1346 .record_access_log(
1347 AccessLogOwner::Server(server_id.as_u32()),
1348 protocol.protocol_name(),
1349 Some(connection_id.as_u32()),
1350 "injected_action",
1351 action,
1352 vec![outcome_json],
1353 )
1354 .await;
1355
1356 match &outcome {
1357 Err(e) => warn!(
1358 "BGP injected action on server #{} connection {} failed: {}",
1359 server_id.as_u32(),
1360 connection_id,
1361 e
1362 ),
1363 Ok(ClientSendOutcome::Disconnected) => {
1364 app_state
1365 .remove_peer_handle(server_id, connection_id.as_u32())
1366 .await;
1367 app_state
1368 .close_connection_on_server(server_id, connection_id)
1369 .await;
1370 }
1371 Ok(_) => {}
1372 }
1373 let _ = status_tx.send("__UPDATE_UI__".to_string());
1374 let _ = command.reply_tx.send(outcome);
1375 }
1376 debug!(
1377 "BGP peer command task for server #{} connection {} ended",
1378 server_id.as_u32(),
1379 connection_id
1380 );
1381 })
1382 .await
1383}
1384
1385async fn execute_injected_action(
1387 action: &serde_json::Value,
1388 protocol: &BgpProtocol,
1389 app_state: &AppState,
1390 server_id: crate::state::ServerId,
1391 out_tx: &mpsc::UnboundedSender<Vec<u8>>,
1392 peer_asn4: bool,
1393 shutdown: &Arc<Shutdown>,
1394) -> Result<crate::state::client_handles::ClientSendOutcome> {
1395 use crate::state::client_handles::ClientSendOutcome;
1396
1397 let cease = |out_tx: &mpsc::UnboundedSender<Vec<u8>>| -> Result<()> {
1398 let bytes =
1399 wire::encode_notification(wire::ERR_CEASE, wire::SUB_CEASE_ADMIN_SHUTDOWN, &[])?;
1400 out_tx
1401 .send(bytes)
1402 .map_err(|_| anyhow!("BGP session already closed"))?;
1403 shutdown.set();
1404 Ok(())
1405 };
1406
1407 let result = crate::llm::actions::executor::execute_actions(
1408 vec![action.clone()],
1409 app_state,
1410 Some(protocol),
1411 Some(server_id),
1412 None,
1413 )
1414 .await?;
1415
1416 if let Some(failure) = result.failures.first() {
1419 return Ok(ClientSendOutcome::Rejected {
1420 error: failure.error.clone(),
1421 });
1422 }
1423
1424 let mut bytes_sent = 0usize;
1425 let mut closed = false;
1426 let mut details: Vec<String> = Vec::new();
1427 let mut queue: Vec<ActionResult> = result.protocol_results;
1428 queue.reverse();
1429 while let Some(item) = queue.pop() {
1430 match item {
1431 ActionResult::Multiple(inner) => {
1432 for item in inner.into_iter().rev() {
1433 queue.push(item);
1434 }
1435 }
1436 ActionResult::Custom { name, data } if name == BGP_MESSAGE_INTENT => {
1437 let bytes = wire::encode_intent(&data, peer_asn4)?;
1438 bytes_sent += bytes.len();
1439 out_tx
1440 .send(bytes)
1441 .map_err(|_| anyhow!("BGP session already closed"))?;
1442 }
1443 ActionResult::Output(bytes) => {
1444 bytes_sent += bytes.len();
1445 out_tx
1446 .send(bytes)
1447 .map_err(|_| anyhow!("BGP session already closed"))?;
1448 }
1449 ActionResult::CloseConnection => {
1450 cease(out_tx)?;
1451 closed = true;
1452 }
1453 ActionResult::WaitForMore | ActionResult::NoAction => {}
1454 other => details.push(format!("{other:?}")),
1455 }
1456 }
1457
1458 Ok(if closed {
1459 ClientSendOutcome::Disconnected
1460 } else if bytes_sent > 0 {
1461 ClientSendOutcome::Sent { bytes_sent }
1462 } else if details.is_empty() {
1463 ClientSendOutcome::Executed {
1464 detail: "executed (nothing to write)".to_string(),
1465 }
1466 } else {
1467 ClientSendOutcome::Executed {
1468 detail: crate::utils::truncate_for_log(&details.join("; "), 160),
1469 }
1470 })
1471}
1472
1473fn notification_codes(n: &netgauze_bgp_pkt::notification::BgpNotificationMessage) -> (u8, u8) {
1476 use netgauze_bgp_pkt::notification::*;
1477 match n {
1478 BgpNotificationMessage::MessageHeaderError(e) => (
1479 wire::ERR_HEADER,
1480 match e {
1481 MessageHeaderError::Unspecific { .. } => 0,
1482 MessageHeaderError::ConnectionNotSynchronized { .. } => 1,
1483 MessageHeaderError::BadMessageLength { .. } => 2,
1484 MessageHeaderError::BadMessageType { .. } => 3,
1485 },
1486 ),
1487 BgpNotificationMessage::OpenMessageError(e) => (
1488 wire::ERR_OPEN,
1489 match e {
1490 OpenMessageError::Unspecific { .. } => 0,
1491 OpenMessageError::UnsupportedVersionNumber { .. } => 1,
1492 OpenMessageError::BadPeerAs { .. } => 2,
1493 OpenMessageError::BadBgpIdentifier { .. } => 3,
1494 OpenMessageError::UnsupportedOptionalParameter { .. } => 4,
1495 OpenMessageError::UnacceptableHoldTime { .. } => 6,
1496 OpenMessageError::UnsupportedCapability { .. } => 7,
1497 OpenMessageError::RoleMismatch { .. } => 11,
1498 },
1499 ),
1500 BgpNotificationMessage::UpdateMessageError(e) => (
1501 wire::ERR_UPDATE,
1502 match e {
1503 UpdateMessageError::Unspecific { .. } => 0,
1504 UpdateMessageError::MalformedAttributeList { .. } => 1,
1505 UpdateMessageError::UnrecognizedWellKnownAttribute { .. } => 2,
1506 UpdateMessageError::MissingWellKnownAttribute { .. } => 3,
1507 UpdateMessageError::AttributeFlagsError { .. } => 4,
1508 UpdateMessageError::AttributeLengthError { .. } => 5,
1509 UpdateMessageError::InvalidOriginAttribute { .. } => 6,
1510 UpdateMessageError::InvalidNextHopAttribute { .. } => 8,
1511 UpdateMessageError::OptionalAttributeError { .. } => 9,
1512 UpdateMessageError::InvalidNetworkField { .. } => 10,
1513 UpdateMessageError::MalformedAsPath { .. } => 11,
1514 },
1515 ),
1516 BgpNotificationMessage::HoldTimerExpiredError(e) => (
1517 wire::ERR_HOLD_TIMER_EXPIRED,
1518 match e {
1519 HoldTimerExpiredError::Unspecific { sub_code, .. } => *sub_code,
1520 },
1521 ),
1522 BgpNotificationMessage::FiniteStateMachineError(e) => (
1523 wire::ERR_FSM,
1524 match e {
1525 FiniteStateMachineError::Unspecific { .. } => 0,
1526 FiniteStateMachineError::ReceiveUnexpectedMessageInOpenSentState { .. } => 1,
1527 FiniteStateMachineError::ReceiveUnexpectedMessageInOpenConfirmState { .. } => 2,
1528 FiniteStateMachineError::ReceiveUnexpectedMessageInEstablishedState { .. } => 3,
1529 },
1530 ),
1531 BgpNotificationMessage::CeaseError(e) => (
1532 wire::ERR_CEASE,
1533 match e {
1534 CeaseError::MaximumNumberOfPrefixesReached { .. } => 1,
1535 CeaseError::AdministrativeShutdown { .. } => 2,
1536 CeaseError::PeerDeConfigured { .. } => 3,
1537 CeaseError::AdministrativeReset { .. } => 4,
1538 CeaseError::ConnectionRejected { .. } => 5,
1539 CeaseError::OtherConfigurationChange { .. } => 6,
1540 CeaseError::ConnectionCollisionResolution { .. } => 7,
1541 CeaseError::OutOfResources { .. } => 8,
1542 CeaseError::HardReset { .. } => 9,
1543 CeaseError::BfdDown { .. } => 10,
1544 },
1545 ),
1546 BgpNotificationMessage::RouteRefreshError(e) => (
1547 7,
1548 match e {
1549 RouteRefreshError::InvalidMessageLength { .. } => 1,
1550 },
1551 ),
1552 }
1553}