/ abzu-daemon / src / stun.rs
stun.rs
  1  //! Sovereign STUN Server
  2  //!
  3  //! Lightweight STUN server for NAT discovery without external dependencies.
  4  //!
  5  //! # Design
  6  //!
  7  //! - Single UDP socket (no multi-IP requirement for basic XorMappedAddress)
  8  //! - Zero-log policy: NO source IPs, NO transaction IDs logged
  9  //! - Uses `stun_proto` v1.0 for protocol handling (aligned with abzu-transport)
 10  
 11  use std::net::SocketAddr;
 12  use std::sync::atomic::{AtomicBool, Ordering};
 13  use std::sync::Arc;
 14  
 15  use stun_proto::types::attribute::XorMappedAddress;
 16  use stun_proto::types::message::{
 17      Message, MessageClass, MessageType, MessageWrite, MessageWriteExt, MessageWriteVec,
 18      TransactionId, BINDING,
 19  };
 20  use tokio::net::UdpSocket;
 21  use tracing::{debug, trace};
 22  
 23  /// Default STUN port per RFC 5389
 24  pub const DEFAULT_STUN_PORT: u16 = 3478;
 25  
 26  /// Sovereign STUN server configuration
 27  #[derive(Debug, Clone)]
 28  pub struct StunServerConfig {
 29      /// UDP bind address (default: 0.0.0.0:3478)
 30      pub bind_addr: SocketAddr,
 31  }
 32  
 33  impl Default for StunServerConfig {
 34      fn default() -> Self {
 35          Self {
 36              bind_addr: ([0, 0, 0, 0], DEFAULT_STUN_PORT).into(),
 37          }
 38      }
 39  }
 40  
 41  /// Run the STUN server
 42  ///
 43  /// Binds to the configured UDP port and responds to STUN Binding Requests.
 44  /// This function runs until the shutdown signal is received.
 45  ///
 46  /// # Zero-Log Policy
 47  ///
 48  /// This server explicitly does NOT log:
 49  /// - Source IP addresses
 50  /// - Transaction IDs
 51  /// - Any identifying information
 52  ///
 53  /// Only aggregate statistics (request count) are logged at trace level.
 54  pub async fn run_stun_server(
 55      config: StunServerConfig,
 56      shutdown: Arc<AtomicBool>,
 57  ) -> std::io::Result<()> {
 58      let socket = UdpSocket::bind(config.bind_addr).await?;
 59  
 60      // Log only the bound address, nothing about clients
 61      debug!(
 62          bound = %config.bind_addr,
 63          "Sovereign STUN server ready"
 64      );
 65  
 66      let mut buf = [0u8; 576]; // RFC recommended minimum
 67      let mut request_count: u64 = 0;
 68  
 69      loop {
 70          // Check shutdown signal
 71          if shutdown.load(Ordering::Relaxed) {
 72              debug!("STUN server shutting down");
 73              break;
 74          }
 75  
 76          // Use timeout to allow periodic shutdown checks
 77          let recv_result = tokio::time::timeout(
 78              std::time::Duration::from_secs(1),
 79              socket.recv_from(&mut buf),
 80          )
 81          .await;
 82  
 83          let (len, source) = match recv_result {
 84              Ok(Ok((len, source))) => (len, source),
 85              Ok(Err(_)) => continue, // I/O error, continue
 86              Err(_) => continue,     // Timeout, check shutdown and continue
 87          };
 88  
 89          // Parse STUN message
 90          let msg: Message<'_> = match Message::from_bytes(&buf[..len]) {
 91              Ok(msg) => msg,
 92              Err(_) => continue, // Invalid STUN message, silently drop
 93          };
 94  
 95          // Only handle Binding Requests
 96          let mtype = msg.get_type();
 97          if mtype.class() != MessageClass::Request || mtype.method() != BINDING {
 98              continue;
 99          }
100  
101          // Build response with XOR-MAPPED-ADDRESS
102          if let Some(response) = build_binding_response(msg.transaction_id(), source) {
103              // Send response (ignore errors, stateless protocol)
104              let _ = socket.send_to(&response, source).await;
105              request_count += 1;
106  
107              // Periodic aggregate stat (no identifying info)
108              if request_count.is_multiple_of(1000) {
109                  trace!(count = request_count, "STUN requests served");
110              }
111          }
112      }
113  
114      Ok(())
115  }
116  
117  /// Build a STUN Binding Success Response with XOR-MAPPED-ADDRESS
118  fn build_binding_response(transaction_id: TransactionId, source: SocketAddr) -> Option<Vec<u8>> {
119      let mtype = MessageType::from_class_method(MessageClass::Success, BINDING);
120      let mut msg_builder = Message::builder(mtype, transaction_id, MessageWriteVec::new());
121  
122      // Create XOR-MAPPED-ADDRESS with socket address
123      let xma = XorMappedAddress::new(source, transaction_id);
124      
125      // Add attribute (mutable operation, returns Result<()>)
126      msg_builder.add_attribute(&xma).ok()?;
127      Some(msg_builder.finish())
128  }
129  
130  #[cfg(test)]
131  mod tests {
132      use super::*;
133      use stun_proto::types::attribute::AttributeStaticType;
134  
135      #[test]
136      fn test_default_config() {
137          let config = StunServerConfig::default();
138          assert_eq!(config.bind_addr.port(), DEFAULT_STUN_PORT);
139      }
140  
141      #[tokio::test]
142      async fn test_build_binding_response() {
143          let tid = TransactionId::generate();
144          let source: SocketAddr = "203.0.113.1:54321".parse().unwrap();
145  
146          let response = build_binding_response(tid, source);
147          assert!(response.is_some());
148  
149          let response = response.unwrap();
150          assert!(!response.is_empty());
151  
152          // Parse the response
153          let msg: Message<'_> = Message::from_bytes(&response).expect("Should parse");
154          assert_eq!(msg.transaction_id(), tid);
155          assert_eq!(msg.get_type().class(), MessageClass::Success);
156          assert_eq!(msg.get_type().method(), BINDING);
157  
158          // Verify XOR-MAPPED-ADDRESS is present
159          let raw_attr = msg.raw_attribute(XorMappedAddress::TYPE);
160          assert!(raw_attr.is_some());
161  
162          // Decode and verify address
163          let xma = XorMappedAddress::try_from(&raw_attr.unwrap()).expect("Should decode");
164          assert_eq!(xma.addr(tid), source);
165      }
166  }