// STUN/TURN attribute types and TLV encoding/decoding // // Implements attributes per RFC 5389 (STUN) and RFC 5766 (TURN). // Each attribute is a Type-Length-Value (TLV) structure: // Type: 2 bytes (attribute type code) // Length: 2 bytes (value length, excluding padding) // Value: variable (padded to 4-byte boundary with zeros) // // XOR-encoded address attributes (XOR-MAPPED-ADDRESS, XOR-PEER-ADDRESS, // XOR-RELAYED-ADDRESS) use the STUN magic cookie and transaction ID // to XOR the address bytes, preventing NAT ALGs from rewriting them. use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use crate::turn::error::TurnError; use crate::turn::stun::MAGIC_COOKIE; // --------------------------------------------------------------------------- // Attribute type codes // --------------------------------------------------------------------------- /// RFC 5389 STUN attribute type codes pub const ATTR_MAPPED_ADDRESS: u16 = 0x0001; pub const ATTR_USERNAME: u16 = 0x0006; pub const ATTR_MESSAGE_INTEGRITY: u16 = 0x0008; pub const ATTR_ERROR_CODE: u16 = 0x0009; pub const ATTR_UNKNOWN_ATTRIBUTES: u16 = 0x000A; pub const ATTR_REALM: u16 = 0x0014; pub const ATTR_NONCE: u16 = 0x0015; pub const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020; pub const ATTR_SOFTWARE: u16 = 0x8022; pub const ATTR_FINGERPRINT: u16 = 0x8028; /// RFC 5766 TURN attribute type codes pub const ATTR_CHANNEL_NUMBER: u16 = 0x000C; pub const ATTR_LIFETIME: u16 = 0x000D; pub const ATTR_XOR_PEER_ADDRESS: u16 = 0x0012; pub const ATTR_DATA: u16 = 0x0013; pub const ATTR_XOR_RELAYED_ADDRESS: u16 = 0x0016; pub const ATTR_REQUESTED_ADDRESS_FAMILY: u16 = 0x0017; pub const ATTR_EVEN_PORT: u16 = 0x0018; pub const ATTR_REQUESTED_TRANSPORT: u16 = 0x0019; pub const ATTR_DONT_FRAGMENT: u16 = 0x001A; // --------------------------------------------------------------------------- // Address family constants // --------------------------------------------------------------------------- const ADDR_FAMILY_IPV4: u8 = 0x01; const ADDR_FAMILY_IPV6: u8 = 0x02; // --------------------------------------------------------------------------- // StunAttribute enum // --------------------------------------------------------------------------- /// A parsed STUN or TURN attribute. /// /// Each variant corresponds to a specific attribute type. Unknown attributes /// are preserved as raw bytes for forwarding or diagnostic purposes. #[derive(Debug, Clone)] pub enum StunAttribute { // ---- RFC 5389 STUN attributes ---- /// MAPPED-ADDRESS (0x0001): The reflexive transport address of the client /// as seen by the server. Uses plain (non-XOR) encoding. MappedAddress(SocketAddr), /// USERNAME (0x0006): The username for message integrity, encoded as UTF-8. Username(String), /// MESSAGE-INTEGRITY (0x0008): 20-byte HMAC-SHA1 over the STUN message /// (up to but not including this attribute). MessageIntegrity([u8; 20]), /// ERROR-CODE (0x0009): Error response code and human-readable reason phrase. /// Code is in range 300-699 per RFC 5389 §15.6. ErrorCode { code: u16, reason: String }, /// UNKNOWN-ATTRIBUTES (0x000A): List of attribute types that the server /// did not understand. Used in 420 Unknown Attribute error responses. UnknownAttributes(Vec), /// REALM (0x0014): The authentication realm, encoded as UTF-8. /// Used with long-term credential mechanism. Realm(String), /// NONCE (0x0015): A server-generated nonce for replay protection. Nonce(String), /// XOR-MAPPED-ADDRESS (0x0020): Same as MAPPED-ADDRESS but XOR-encoded /// with the magic cookie (and transaction ID for IPv6) to prevent /// NAT ALG interference. XorMappedAddress(SocketAddr), /// SOFTWARE (0x8022): Textual description of the software being used. /// Informational only. Software(String), /// FINGERPRINT (0x8028): CRC32 of the STUN message XORed with 0x5354554e. /// Used to demultiplex STUN from other protocols on the same port. Fingerprint(u32), // ---- RFC 5766 TURN attributes ---- /// CHANNEL-NUMBER (0x000C): Channel number for ChannelBind. /// Must be in range 0x4000-0x7FFF. ChannelNumber(u16), /// LIFETIME (0x000D): Requested or granted allocation lifetime in seconds. Lifetime(u32), /// XOR-PEER-ADDRESS (0x0012): The peer address for Send/Data indications, /// CreatePermission, and ChannelBind. XOR-encoded like XOR-MAPPED-ADDRESS. XorPeerAddress(SocketAddr), /// DATA (0x0013): The application data payload in Send/Data indications. Data(Vec), /// XOR-RELAYED-ADDRESS (0x0016): The relayed transport address allocated /// by the server. XOR-encoded. XorRelayedAddress(SocketAddr), /// EVEN-PORT (0x0018): Requests an even port number for the relay address. /// The boolean indicates the R bit (reserve next-higher port). EvenPort(bool), /// REQUESTED-TRANSPORT (0x0019): The transport protocol for the relay. /// Value is an IANA protocol number (17 = UDP, 6 = TCP). RequestedTransport(u8), /// DONT-FRAGMENT (0x001A): Requests that the server set the DF bit /// in outgoing UDP packets. No value (zero-length attribute). DontFragment, /// REQUESTED-ADDRESS-FAMILY (0x0017): Requests a specific address family /// for the relayed address (0x01 = IPv4, 0x02 = IPv6). RequestedAddressFamily(u8), /// Unknown/unsupported attribute preserved as raw bytes. Unknown { attr_type: u16, value: Vec }, } // --------------------------------------------------------------------------- // Decoding // --------------------------------------------------------------------------- /// Decode a single attribute from its type code and raw value bytes. /// /// The `transaction_id` is needed for XOR address decoding. pub fn decode_attribute( attr_type: u16, value: &[u8], transaction_id: &[u8; 12], ) -> Result { match attr_type { ATTR_MAPPED_ADDRESS => decode_mapped_address(value), ATTR_USERNAME => decode_utf8_string(value).map(StunAttribute::Username), ATTR_MESSAGE_INTEGRITY => decode_message_integrity(value), ATTR_ERROR_CODE => decode_error_code(value), ATTR_UNKNOWN_ATTRIBUTES => decode_unknown_attributes(value), ATTR_REALM => decode_utf8_string(value).map(StunAttribute::Realm), ATTR_NONCE => decode_utf8_string(value).map(StunAttribute::Nonce), ATTR_XOR_MAPPED_ADDRESS => { decode_xor_address(value, transaction_id).map(StunAttribute::XorMappedAddress) } ATTR_SOFTWARE => decode_utf8_string(value).map(StunAttribute::Software), ATTR_FINGERPRINT => decode_fingerprint(value), ATTR_CHANNEL_NUMBER => decode_channel_number(value), ATTR_LIFETIME => decode_lifetime(value), ATTR_XOR_PEER_ADDRESS => { decode_xor_address(value, transaction_id).map(StunAttribute::XorPeerAddress) } ATTR_DATA => Ok(StunAttribute::Data(value.to_vec())), ATTR_XOR_RELAYED_ADDRESS => { decode_xor_address(value, transaction_id).map(StunAttribute::XorRelayedAddress) } ATTR_REQUESTED_ADDRESS_FAMILY => decode_requested_address_family(value), ATTR_EVEN_PORT => decode_even_port(value), ATTR_REQUESTED_TRANSPORT => decode_requested_transport(value), ATTR_DONT_FRAGMENT => Ok(StunAttribute::DontFragment), _ => Ok(StunAttribute::Unknown { attr_type, value: value.to_vec(), }), } } // --------------------------------------------------------------------------- // Encoding // --------------------------------------------------------------------------- /// Encode a single attribute into TLV wire format with 4-byte padding. /// /// Returns the complete TLV bytes: type (2) + length (2) + value + padding. pub fn encode_attribute(attr: &StunAttribute, transaction_id: &[u8; 12]) -> Vec { let (attr_type, value) = encode_attribute_value(attr, transaction_id); let value_len = value.len(); let padded_len = (value_len + 3) & !3; let mut buf = Vec::with_capacity(4 + padded_len); buf.extend_from_slice(&attr_type.to_be_bytes()); buf.extend_from_slice(&(value_len as u16).to_be_bytes()); buf.extend_from_slice(&value); // Pad to 4-byte boundary let padding = padded_len - value_len; for _ in 0..padding { buf.push(0); } buf } /// Encode an attribute's value bytes (without the TLV header or padding). /// Returns (attribute_type_code, value_bytes). fn encode_attribute_value(attr: &StunAttribute, transaction_id: &[u8; 12]) -> (u16, Vec) { match attr { StunAttribute::MappedAddress(addr) => { (ATTR_MAPPED_ADDRESS, encode_plain_address(addr)) } StunAttribute::Username(s) => (ATTR_USERNAME, s.as_bytes().to_vec()), StunAttribute::MessageIntegrity(hmac) => (ATTR_MESSAGE_INTEGRITY, hmac.to_vec()), StunAttribute::ErrorCode { code, reason } => { (ATTR_ERROR_CODE, encode_error_code(*code, reason)) } StunAttribute::UnknownAttributes(types) => { let mut buf = Vec::with_capacity(types.len() * 2); for &t in types { buf.extend_from_slice(&t.to_be_bytes()); } (ATTR_UNKNOWN_ATTRIBUTES, buf) } StunAttribute::Realm(s) => (ATTR_REALM, s.as_bytes().to_vec()), StunAttribute::Nonce(s) => (ATTR_NONCE, s.as_bytes().to_vec()), StunAttribute::XorMappedAddress(addr) => { (ATTR_XOR_MAPPED_ADDRESS, encode_xor_address(addr, transaction_id)) } StunAttribute::Software(s) => (ATTR_SOFTWARE, s.as_bytes().to_vec()), StunAttribute::Fingerprint(val) => (ATTR_FINGERPRINT, val.to_be_bytes().to_vec()), StunAttribute::ChannelNumber(num) => { // Channel number is 16 bits followed by 16 bits of RFFU (reserved) let mut buf = vec![0u8; 4]; buf[0..2].copy_from_slice(&num.to_be_bytes()); (ATTR_CHANNEL_NUMBER, buf) } StunAttribute::Lifetime(secs) => (ATTR_LIFETIME, secs.to_be_bytes().to_vec()), StunAttribute::XorPeerAddress(addr) => { (ATTR_XOR_PEER_ADDRESS, encode_xor_address(addr, transaction_id)) } StunAttribute::Data(data) => (ATTR_DATA, data.clone()), StunAttribute::XorRelayedAddress(addr) => { (ATTR_XOR_RELAYED_ADDRESS, encode_xor_address(addr, transaction_id)) } StunAttribute::RequestedAddressFamily(family) => { let mut buf = vec![0u8; 4]; buf[0] = *family; (ATTR_REQUESTED_ADDRESS_FAMILY, buf) } StunAttribute::EvenPort(reserve) => { let byte = if *reserve { 0x80 } else { 0x00 }; (ATTR_EVEN_PORT, vec![byte]) } StunAttribute::RequestedTransport(proto) => { // Protocol number in first byte, followed by 3 bytes RFFU let mut buf = vec![0u8; 4]; buf[0] = *proto; (ATTR_REQUESTED_TRANSPORT, buf) } StunAttribute::DontFragment => (ATTR_DONT_FRAGMENT, vec![]), StunAttribute::Unknown { attr_type, value } => (*attr_type, value.clone()), } } // --------------------------------------------------------------------------- // Address encoding/decoding helpers // --------------------------------------------------------------------------- /// Decode a plain (non-XOR) MAPPED-ADDRESS attribute value. /// /// Format: 1 byte reserved, 1 byte family, 2 bytes port, 4/16 bytes address fn decode_mapped_address(value: &[u8]) -> Result { if value.len() < 4 { return Err(TurnError::StunParseError( "MAPPED-ADDRESS too short".into(), )); } let family = value[1]; let port = u16::from_be_bytes([value[2], value[3]]); let addr = match family { ADDR_FAMILY_IPV4 => { if value.len() < 8 { return Err(TurnError::StunParseError( "MAPPED-ADDRESS IPv4 too short".into(), )); } let ip = Ipv4Addr::new(value[4], value[5], value[6], value[7]); SocketAddr::new(IpAddr::V4(ip), port) } ADDR_FAMILY_IPV6 => { if value.len() < 20 { return Err(TurnError::StunParseError( "MAPPED-ADDRESS IPv6 too short".into(), )); } let mut octets = [0u8; 16]; octets.copy_from_slice(&value[4..20]); let ip = Ipv6Addr::from(octets); SocketAddr::new(IpAddr::V6(ip), port) } _ => { return Err(TurnError::StunParseError(format!( "unknown address family: 0x{:02x}", family ))); } }; Ok(StunAttribute::MappedAddress(addr)) } /// Encode a plain (non-XOR) address into wire format. fn encode_plain_address(addr: &SocketAddr) -> Vec { match addr { SocketAddr::V4(v4) => { let mut buf = vec![0u8; 8]; buf[0] = 0; // reserved buf[1] = ADDR_FAMILY_IPV4; buf[2..4].copy_from_slice(&v4.port().to_be_bytes()); buf[4..8].copy_from_slice(&v4.ip().octets()); buf } SocketAddr::V6(v6) => { let mut buf = vec![0u8; 20]; buf[0] = 0; // reserved buf[1] = ADDR_FAMILY_IPV6; buf[2..4].copy_from_slice(&v6.port().to_be_bytes()); buf[4..20].copy_from_slice(&v6.ip().octets()); buf } } } /// Decode an XOR-encoded address (XOR-MAPPED-ADDRESS, XOR-PEER-ADDRESS, /// XOR-RELAYED-ADDRESS) per RFC 5389 §15.2. /// /// For IPv4: port is XORed with top 16 bits of magic cookie; /// address is XORed with magic cookie. /// For IPv6: port is XORed with top 16 bits of magic cookie; /// address is XORed with magic cookie || transaction ID (16 bytes). fn decode_xor_address( value: &[u8], transaction_id: &[u8; 12], ) -> Result { if value.len() < 4 { return Err(TurnError::StunParseError( "XOR address attribute too short".into(), )); } let family = value[1]; let x_port = u16::from_be_bytes([value[2], value[3]]); let cookie_bytes = MAGIC_COOKIE.to_be_bytes(); let port = x_port ^ u16::from_be_bytes([cookie_bytes[0], cookie_bytes[1]]); match family { ADDR_FAMILY_IPV4 => { if value.len() < 8 { return Err(TurnError::StunParseError( "XOR-MAPPED-ADDRESS IPv4 too short".into(), )); } let x_addr = u32::from_be_bytes([value[4], value[5], value[6], value[7]]); let addr = x_addr ^ MAGIC_COOKIE; let ip = Ipv4Addr::from(addr); Ok(SocketAddr::new(IpAddr::V4(ip), port)) } ADDR_FAMILY_IPV6 => { if value.len() < 20 { return Err(TurnError::StunParseError( "XOR-MAPPED-ADDRESS IPv6 too short".into(), )); } // Build the 16-byte XOR mask: magic cookie (4 bytes) + transaction ID (12 bytes) let mut xor_mask = [0u8; 16]; xor_mask[0..4].copy_from_slice(&cookie_bytes); xor_mask[4..16].copy_from_slice(transaction_id); let mut addr_bytes = [0u8; 16]; for i in 0..16 { addr_bytes[i] = value[4 + i] ^ xor_mask[i]; } let ip = Ipv6Addr::from(addr_bytes); Ok(SocketAddr::new(IpAddr::V6(ip), port)) } _ => Err(TurnError::StunParseError(format!( "unknown address family in XOR address: 0x{:02x}", family ))), } } /// Encode an address using XOR encoding per RFC 5389 §15.2. fn encode_xor_address(addr: &SocketAddr, transaction_id: &[u8; 12]) -> Vec { let cookie_bytes = MAGIC_COOKIE.to_be_bytes(); let x_port = addr.port() ^ u16::from_be_bytes([cookie_bytes[0], cookie_bytes[1]]); match addr { SocketAddr::V4(v4) => { let mut buf = vec![0u8; 8]; buf[0] = 0; // reserved buf[1] = ADDR_FAMILY_IPV4; buf[2..4].copy_from_slice(&x_port.to_be_bytes()); let x_addr = u32::from_be_bytes(v4.ip().octets()) ^ MAGIC_COOKIE; buf[4..8].copy_from_slice(&x_addr.to_be_bytes()); buf } SocketAddr::V6(v6) => { let mut buf = vec![0u8; 20]; buf[0] = 0; // reserved buf[1] = ADDR_FAMILY_IPV6; buf[2..4].copy_from_slice(&x_port.to_be_bytes()); // XOR mask: magic cookie (4 bytes) + transaction ID (12 bytes) let mut xor_mask = [0u8; 16]; xor_mask[0..4].copy_from_slice(&cookie_bytes); xor_mask[4..16].copy_from_slice(transaction_id); let octets = v6.ip().octets(); for i in 0..16 { buf[4 + i] = octets[i] ^ xor_mask[i]; } buf } } } // --------------------------------------------------------------------------- // Specific attribute decoders // --------------------------------------------------------------------------- /// Decode UTF-8 string from raw bytes. fn decode_utf8_string(value: &[u8]) -> Result { String::from_utf8(value.to_vec()).map_err(|e| { TurnError::StunParseError(format!("invalid UTF-8 in attribute: {}", e)) }) } /// Decode MESSAGE-INTEGRITY (exactly 20 bytes HMAC-SHA1). fn decode_message_integrity(value: &[u8]) -> Result { if value.len() != 20 { return Err(TurnError::StunParseError(format!( "MESSAGE-INTEGRITY must be 20 bytes, got {}", value.len() ))); } let mut hmac = [0u8; 20]; hmac.copy_from_slice(value); Ok(StunAttribute::MessageIntegrity(hmac)) } /// Decode ERROR-CODE attribute per RFC 5389 §15.6. /// /// Format: 2 bytes reserved, 1 byte with class (hundreds digit) in bits 0-2, /// 1 byte with number (tens+units), followed by UTF-8 reason phrase. fn decode_error_code(value: &[u8]) -> Result { if value.len() < 4 { return Err(TurnError::StunParseError( "ERROR-CODE too short".into(), )); } let class = (value[2] & 0x07) as u16; let number = value[3] as u16; let code = class * 100 + number; let reason = if value.len() > 4 { String::from_utf8_lossy(&value[4..]).into_owned() } else { String::new() }; Ok(StunAttribute::ErrorCode { code, reason }) } /// Encode ERROR-CODE value bytes per RFC 5389 §15.6. fn encode_error_code(code: u16, reason: &str) -> Vec { let class = (code / 100) as u8; let number = (code % 100) as u8; let reason_bytes = reason.as_bytes(); let mut buf = Vec::with_capacity(4 + reason_bytes.len()); buf.push(0); // reserved buf.push(0); // reserved buf.push(class & 0x07); buf.push(number); buf.extend_from_slice(reason_bytes); buf } /// Decode UNKNOWN-ATTRIBUTES (list of 16-bit attribute types). fn decode_unknown_attributes(value: &[u8]) -> Result { if value.len() % 2 != 0 { return Err(TurnError::StunParseError( "UNKNOWN-ATTRIBUTES length must be even".into(), )); } let mut types = Vec::with_capacity(value.len() / 2); for chunk in value.chunks_exact(2) { types.push(u16::from_be_bytes([chunk[0], chunk[1]])); } Ok(StunAttribute::UnknownAttributes(types)) } /// Decode FINGERPRINT (4-byte CRC32 XOR value). fn decode_fingerprint(value: &[u8]) -> Result { if value.len() != 4 { return Err(TurnError::StunParseError(format!( "FINGERPRINT must be 4 bytes, got {}", value.len() ))); } let val = u32::from_be_bytes([value[0], value[1], value[2], value[3]]); Ok(StunAttribute::Fingerprint(val)) } /// Decode CHANNEL-NUMBER (16-bit number + 16-bit RFFU). fn decode_channel_number(value: &[u8]) -> Result { if value.len() < 4 { return Err(TurnError::StunParseError( "CHANNEL-NUMBER too short".into(), )); } let num = u16::from_be_bytes([value[0], value[1]]); Ok(StunAttribute::ChannelNumber(num)) } /// Decode LIFETIME (32-bit seconds). fn decode_lifetime(value: &[u8]) -> Result { if value.len() < 4 { return Err(TurnError::StunParseError("LIFETIME too short".into())); } let secs = u32::from_be_bytes([value[0], value[1], value[2], value[3]]); Ok(StunAttribute::Lifetime(secs)) } /// Decode REQUESTED-ADDRESS-FAMILY (1 byte family + 3 bytes RFFU). fn decode_requested_address_family(value: &[u8]) -> Result { if value.is_empty() { return Err(TurnError::StunParseError( "REQUESTED-ADDRESS-FAMILY empty".into(), )); } Ok(StunAttribute::RequestedAddressFamily(value[0])) } /// Decode EVEN-PORT (1 byte, R bit in most significant bit). fn decode_even_port(value: &[u8]) -> Result { if value.is_empty() { return Err(TurnError::StunParseError("EVEN-PORT empty".into())); } let reserve = (value[0] & 0x80) != 0; Ok(StunAttribute::EvenPort(reserve)) } /// Decode REQUESTED-TRANSPORT (1 byte protocol + 3 bytes RFFU). fn decode_requested_transport(value: &[u8]) -> Result { if value.is_empty() { return Err(TurnError::StunParseError( "REQUESTED-TRANSPORT empty".into(), )); } Ok(StunAttribute::RequestedTransport(value[0])) } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; #[test] fn test_xor_mapped_address_ipv4_roundtrip() { let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 100), 12345)); let txn_id = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; let encoded = encode_xor_address(&addr, &txn_id); let decoded = decode_xor_address(&encoded, &txn_id).unwrap(); assert_eq!(decoded, addr); } #[test] fn test_xor_mapped_address_ipv6_roundtrip() { let ip = Ipv6Addr::new(0x2001, 0x0db8, 0x85a3, 0, 0, 0x8a2e, 0x0370, 0x7334); let addr = SocketAddr::V6(SocketAddrV6::new(ip, 54321, 0, 0)); let txn_id = [0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]; let encoded = encode_xor_address(&addr, &txn_id); let decoded = decode_xor_address(&encoded, &txn_id).unwrap(); assert_eq!(decoded, addr); } #[test] fn test_plain_address_ipv4_roundtrip() { let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 8080)); let encoded = encode_plain_address(&addr); let decoded = decode_mapped_address(&encoded).unwrap(); if let StunAttribute::MappedAddress(decoded_addr) = decoded { assert_eq!(decoded_addr, addr); } else { panic!("expected MappedAddress"); } } #[test] fn test_error_code_roundtrip() { let encoded = encode_error_code(401, "Unauthorized"); let decoded = decode_error_code(&encoded).unwrap(); if let StunAttribute::ErrorCode { code, reason } = decoded { assert_eq!(code, 401); assert_eq!(reason, "Unauthorized"); } else { panic!("expected ErrorCode"); } } #[test] fn test_error_code_438() { let encoded = encode_error_code(438, "Stale Nonce"); let decoded = decode_error_code(&encoded).unwrap(); if let StunAttribute::ErrorCode { code, reason } = decoded { assert_eq!(code, 438); assert_eq!(reason, "Stale Nonce"); } else { panic!("expected ErrorCode"); } } #[test] fn test_attribute_tlv_encoding() { let txn_id = [0u8; 12]; let attr = StunAttribute::Username("alice".into()); let encoded = encode_attribute(&attr, &txn_id); // Type (2) + Length (2) + "alice" (5) + padding (3) = 12 assert_eq!(encoded.len(), 12); assert_eq!(encoded[0..2], ATTR_USERNAME.to_be_bytes()); assert_eq!(encoded[2..4], 5u16.to_be_bytes()); assert_eq!(&encoded[4..9], b"alice"); assert_eq!(encoded[9], 0); // padding assert_eq!(encoded[10], 0); assert_eq!(encoded[11], 0); } #[test] fn test_attribute_tlv_encoding_4_byte_aligned() { let txn_id = [0u8; 12]; let attr = StunAttribute::Username("test".into()); let encoded = encode_attribute(&attr, &txn_id); // Type (2) + Length (2) + "test" (4) = 8, already aligned assert_eq!(encoded.len(), 8); } #[test] fn test_lifetime_roundtrip() { let attr = StunAttribute::Lifetime(600); let txn_id = [0u8; 12]; let encoded = encode_attribute(&attr, &txn_id); // Type (2) + Length (2) + value (4) = 8 assert_eq!(encoded.len(), 8); let decoded = decode_attribute(ATTR_LIFETIME, &encoded[4..8], &txn_id).unwrap(); if let StunAttribute::Lifetime(secs) = decoded { assert_eq!(secs, 600); } else { panic!("expected Lifetime"); } } #[test] fn test_channel_number_roundtrip() { let attr = StunAttribute::ChannelNumber(0x4000); let txn_id = [0u8; 12]; let encoded = encode_attribute(&attr, &txn_id); // Type (2) + Length (2) + value (4) = 8 assert_eq!(encoded.len(), 8); let decoded = decode_attribute(ATTR_CHANNEL_NUMBER, &encoded[4..8], &txn_id).unwrap(); if let StunAttribute::ChannelNumber(num) = decoded { assert_eq!(num, 0x4000); } else { panic!("expected ChannelNumber"); } } #[test] fn test_requested_transport_udp() { let attr = StunAttribute::RequestedTransport(17); // UDP let txn_id = [0u8; 12]; let encoded = encode_attribute(&attr, &txn_id); let decoded = decode_attribute(ATTR_REQUESTED_TRANSPORT, &encoded[4..8], &txn_id).unwrap(); if let StunAttribute::RequestedTransport(proto) = decoded { assert_eq!(proto, 17); } else { panic!("expected RequestedTransport"); } } #[test] fn test_dont_fragment() { let attr = StunAttribute::DontFragment; let txn_id = [0u8; 12]; let encoded = encode_attribute(&attr, &txn_id); // Type (2) + Length (2) + no value = 4 assert_eq!(encoded.len(), 4); assert_eq!(encoded[2..4], 0u16.to_be_bytes()); // length = 0 } #[test] fn test_unknown_attribute_preserved() { let txn_id = [0u8; 12]; let decoded = decode_attribute(0xFFFF, &[0x01, 0x02, 0x03], &txn_id).unwrap(); if let StunAttribute::Unknown { attr_type, value } = decoded { assert_eq!(attr_type, 0xFFFF); assert_eq!(value, vec![0x01, 0x02, 0x03]); } else { panic!("expected Unknown"); } } #[test] fn test_message_integrity_decode() { let hmac = [0xAA; 20]; let decoded = decode_message_integrity(&hmac).unwrap(); if let StunAttribute::MessageIntegrity(h) = decoded { assert_eq!(h, [0xAA; 20]); } else { panic!("expected MessageIntegrity"); } } #[test] fn test_message_integrity_wrong_length() { let result = decode_message_integrity(&[0; 10]); assert!(result.is_err()); } #[test] fn test_fingerprint_roundtrip() { let val = 0xDEADBEEF_u32; let encoded_value = val.to_be_bytes().to_vec(); let decoded = decode_fingerprint(&encoded_value).unwrap(); if let StunAttribute::Fingerprint(v) = decoded { assert_eq!(v, val); } else { panic!("expected Fingerprint"); } } #[test] fn test_even_port_reserve_bit() { // R bit set let decoded = decode_even_port(&[0x80]).unwrap(); if let StunAttribute::EvenPort(reserve) = decoded { assert!(reserve); } else { panic!("expected EvenPort"); } // R bit not set let decoded = decode_even_port(&[0x00]).unwrap(); if let StunAttribute::EvenPort(reserve) = decoded { assert!(!reserve); } else { panic!("expected EvenPort"); } } #[test] fn test_unknown_attributes_list() { let value = vec![0x00, 0x01, 0x00, 0x20]; // MAPPED-ADDRESS, XOR-MAPPED-ADDRESS let decoded = decode_unknown_attributes(&value).unwrap(); if let StunAttribute::UnknownAttributes(types) = decoded { assert_eq!(types, vec![0x0001, 0x0020]); } else { panic!("expected UnknownAttributes"); } } }