From ed882ede681d49cc791623e881ae7126984b1cd0 Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Sun, 15 Feb 2026 08:40:10 -0500 Subject: [PATCH] output/ipv6: Utility function to compress IPv6 addrs Issue: 7399 Utility function to compress IPv6 addresses per RFC-5952 --- rust/src/utils/ipaddr.rs | 188 +++++++++++++++++++++++++++++++++++++++ rust/src/utils/mod.rs | 1 + src/util-print.c | 13 ++- src/util-print.h | 3 +- 4 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 rust/src/utils/ipaddr.rs diff --git a/rust/src/utils/ipaddr.rs b/rust/src/utils/ipaddr.rs new file mode 100644 index 0000000000..0caf78f942 --- /dev/null +++ b/rust/src/utils/ipaddr.rs @@ -0,0 +1,188 @@ +/* Copyright (C) 2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +// Author: Jeff Lucovsky + +use std::ffi::{c_char, c_uchar}; +use std::net::Ipv6Addr; + +/// Writes the compressed IPv6 string into `out_buf`, which has length `out_len`. +/// Returns the number of bytes written (excluding the null terminator), +/// or 0 on error (invalid addr pointer, buffer too small, etc.) +#[no_mangle] +pub unsafe extern "C" fn SCIPv6Compress( + addr: *const c_uchar, // pointer to 16-byte IPv6 + out_buf: *mut c_char, // out buffer allocated by caller + out_len: usize, // size of out buffer +) -> usize { + if addr.is_null() || out_buf.is_null() || out_len == 0 { + return 0; + } + + // get 16-byte IPv6 address + let fixed = std::ptr::read_unaligned(addr as *const [u8; 16]); + let ipv6 = Ipv6Addr::from(fixed); + + let ipv6_str = ipv6.to_string(); + // Sufficient room? + if ipv6_str.len() + 1 > out_len { + return 0; + } + // Copy string + NULL termination + let len = ipv6_str.len(); + crate::ffi::strings::copy_to_c_char(ipv6_str, out_buf, out_len); + len +} + +#[cfg(test)] +mod tests { + + use super::*; + use std::os::raw::{c_char, c_uchar}; + + struct CompressedResult { + string: String, + } + + fn call_compresser(bytes: &[u8; 16]) -> Option { + let mut out = vec![0u8; 64]; + let len = unsafe { + SCIPv6Compress( + bytes.as_ptr() as *const c_uchar, + out.as_mut_ptr() as *mut c_char, + out.len(), + ) + }; + + if len == 0 { + return None; + } + + let s = String::from_utf8(out[..len].to_vec()).unwrap(); + Some(CompressedResult { string: s }) + } + + #[test] + fn test_ipv6_compress_success_zero_addr() { + let addr = [0u8; 16]; + let out = call_compresser(&addr).unwrap(); + assert_eq!(out.string, "::"); + assert_eq!(out.string.len(), 2); + } + + #[test] + fn test_ipv6_compress_success_loopback() { + let mut addr = [0u8; 16]; + addr[15] = 1; // ::1 + let out = call_compresser(&addr).unwrap(); + assert_eq!(out.string, "::1"); + assert_eq!(out.string.len(), 3); + } + + #[test] + fn test_ipv6_compress_success_normal_addr() { + let addr = [ + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, + ]; + let out = call_compresser(&addr).unwrap(); + assert_eq!(out.string, "2001:db8::1"); + assert_eq!(out.string.len(), 11); + } + + #[test] + fn test_ipv6_compress_success_ipv4_mapped() { + let addr: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 168, 0, 1]; + let out = call_compresser(&addr).unwrap(); + assert_eq!(out.string, "::ffff:192.168.0.1"); + assert_eq!(out.string.len(), 18); + } + + #[test] + fn test_ipv6_compress_success_no_zero_compression() { + // 2001:db8:1:2:3:4:5:6 (fully expanded, no zero-run) + let addr: [u8; 16] = [ + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, + 0x00, 0x06, + ]; + let out = call_compresser(&addr).unwrap(); + assert_eq!(out.string, "2001:db8:1:2:3:4:5:6"); + assert_eq!(out.string.len(), 20); + } + + #[test] + fn test_ipv6_compress_fail_null_addr() { + let mut out = vec![0u8; 64]; + let len = + unsafe { SCIPv6Compress(std::ptr::null(), out.as_mut_ptr() as *mut c_char, out.len()) }; + assert_eq!(len, 0); + } + + #[test] + fn test_ipv6_compress_fail_null_out_buf() { + let addr = [0u8; 16]; + let len = + unsafe { SCIPv6Compress(addr.as_ptr() as *const c_uchar, std::ptr::null_mut(), 64) }; + assert_eq!(len, 0); + } + + #[test] + fn test_ipv6_compress_fail_out_buf_too_small() { + let addr = [0u8; 16]; + // "::" is 2 bytes + NUL = 3, so give only length 2 + let mut out = vec![0u8; 2]; + let len = unsafe { + SCIPv6Compress( + addr.as_ptr() as *const c_uchar, + out.as_mut_ptr() as *mut c_char, + out.len(), + ) + }; + assert_eq!(len, 0); + } + + #[test] + fn test_ipv6_compress_fail_exactly_one_byte_short() { + let addr = [0u8; 16]; + let compressed = "::"; + let needed = compressed.len() + 1; // for NUL + let mut out = vec![0u8; needed - 1]; // one byte too small + let len = unsafe { + SCIPv6Compress( + addr.as_ptr() as *const c_uchar, + out.as_mut_ptr() as *mut c_char, + out.len(), + ) + }; + assert_eq!(len, 0); + } + + #[test] + fn test_ipv6_compress_writes_nul_terminator() { + let addr = [0u8; 16]; + let mut out = vec![0u8; 64]; + let len = unsafe { + SCIPv6Compress( + addr.as_ptr() as *const c_uchar, + out.as_mut_ptr() as *mut c_char, + out.len(), + ) + }; + assert!(len > 0); + assert_eq!(out[len], 0); + } +} diff --git a/rust/src/utils/mod.rs b/rust/src/utils/mod.rs index 882126ca50..542aa7791f 100644 --- a/rust/src/utils/mod.rs +++ b/rust/src/utils/mod.rs @@ -17,3 +17,4 @@ pub mod base64; pub mod datalink; +pub mod ipaddr; diff --git a/src/util-print.c b/src/util-print.c index b7f33583d8..1baae9e0bd 100644 --- a/src/util-print.c +++ b/src/util-print.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2023 Open Information Security Foundation +/* Copyright (C) 2007-2026 Open Information Security Foundation * * You can copy, redistribute or modify this Program under the terms of * the GNU General Public License version 2 as published by the Free @@ -206,8 +206,15 @@ void PrintStringsToBuffer(uint8_t *dst_buf, uint32_t *dst_buf_offset_ptr, uint32 dst_buf[dst_buf_size - 1] = 0; } -static const char *PrintInetIPv6(const void *src, char *dst, socklen_t size) +const char *PrintInetIPv6(const void *src, char *dst, socklen_t size, bool compress_ipv6) { + if (compress_ipv6) { + if (SCIPv6Compress(src, dst, size)) { + return dst; + } + // If we can't compress it, use expanded-form */ + } + char s_part[6]; uint16_t x[8]; memcpy(&x, src, 16); @@ -245,7 +252,7 @@ const char *PrintInet(int af, const void *src, char *dst, socklen_t size) #endif case AF_INET6: /* Format IPv6 without deleting zeroes */ - return PrintInetIPv6(src, dst, size); + return PrintInetIPv6(src, dst, size, false); default: SCLogError("Unsupported protocol: %d", af); } diff --git a/src/util-print.h b/src/util-print.h index e589a0a2dd..dd1cf4a760 100644 --- a/src/util-print.h +++ b/src/util-print.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2010 Open Information Security Foundation +/* Copyright (C) 2007-2026 Open Information Security Foundation * * You can copy, redistribute or modify this Program under the terms of * the GNU General Public License version 2 as published by the Free @@ -49,6 +49,7 @@ void PrintStringsToBuffer(uint8_t *dst_buf, uint32_t *dst_buf_offset_ptr, uint32 const uint8_t *src_buf, const uint32_t src_buf_len); void PrintRawLineHexBuf(char *, uint32_t, const uint8_t *, uint32_t ); const char *PrintInet(int , const void *, char *, socklen_t); +const char *PrintInetIPv6(const void *, char *, socklen_t, bool); void PrintHexString(char *str, size_t size, uint8_t *buf, size_t buf_len); #endif /* SURICATA_UTIL_PRINT_H */