From: Philippe Antoine Date: Tue, 15 Apr 2025 12:12:19 +0000 (+0200) Subject: rust: derive for AppLayerState X-Git-Tag: suricata-8.0.0-rc1~416 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bbc007b4d4958aa972253cd37823219e5b58cb68;p=thirdparty%2Fsuricata.git rust: derive for AppLayerState To enable easily hooks for rust app-layers such as SSH --- diff --git a/rust/derive/src/applayerstate.rs b/rust/derive/src/applayerstate.rs new file mode 100644 index 0000000000..58e3cebb2e --- /dev/null +++ b/rust/derive/src/applayerstate.rs @@ -0,0 +1,150 @@ +/* Copyright (C) 2025 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. + */ + +extern crate proc_macro; +use proc_macro::TokenStream; +use quote::quote; +use syn::{self, parse_macro_input, DeriveInput}; + +fn get_attr_strip_prefix(attr: &syn::Attribute) -> String { + let meta = attr.parse_meta().unwrap(); + if let syn::Meta::List(l) = meta { + for n in l.nested { + if let syn::NestedMeta::Meta(m2) = n { + if let syn::Meta::NameValue(nv) = m2 { + if nv.path.is_ident("alstate_strip_prefix") { + if let syn::Lit::Str(s) = nv.lit { + return s.value(); + } + panic!("strip_prefix invalid syntax"); + } + } + } + } + panic!("no strip_prefix"); + } + panic!("suricata attribute is not a list"); +} + +pub fn derive_app_layer_state(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + let mut fields = Vec::new(); + let mut vals = Vec::new(); + let mut cstrings_toserver = Vec::new(); + let mut cstrings_toclient = Vec::new(); + let mut names = Vec::new(); + let mut strip_prefix = String::from(""); + + match input.data { + syn::Data::Enum(ref data) => { + for attr in input.attrs.iter() { + if attr.path.is_ident("suricata") { + strip_prefix = get_attr_strip_prefix(attr); + } + } + for (i, v) in (&data.variants).into_iter().enumerate() { + fields.push(v.ident.clone()); + let name = transform_name(&v.ident.to_string(), &strip_prefix); + let cname_toserver = format!("request_{}\0", name); + let cname_toclient = format!("response_{}\0", name); + names.push(name); + cstrings_toserver.push(cname_toserver); + cstrings_toclient.push(cname_toclient); + vals.push(i as u8); + } + } + _ => panic!("AppLayerState can only be derived for enums"), + } + + let expanded = quote! { + impl crate::applayer::AppLayerState for #name { + fn from_u8(val: u8) -> Option { + match val { + #( #vals => Some(#name::#fields) ,)* + _ => None, + } + } + + fn as_u8(&self) -> u8 { + match *self { + #( #name::#fields => #vals ,)* + } + } + + fn to_cstring(&self, to_server: bool) -> *const std::os::raw::c_char { + let s = if to_server { + match *self { + #( #name::#fields => #cstrings_toserver ,)* + } + } else { + match *self { + #( #name::#fields => #cstrings_toclient ,)* + } + }; + s.as_ptr() as *const std::os::raw::c_char + } + + fn from_str(s: &str) -> Option<#name> { + match s { + #( #names => Some(#name::#fields) ,)* + _ => None + } + } + } + }; + + proc_macro::TokenStream::from(expanded) +} + +fn transform_name(name: &str, strip_prefix: &str) -> String { + if !name.starts_with(strip_prefix) { + panic!("strip prefix is not good") + } + let mut xname = String::new(); + let chars: Vec = name[strip_prefix.len()..].chars().collect(); + for i in 0..chars.len() { + if i > 0 && i < chars.len() - 1 && chars[i].is_uppercase() && chars[i + 1].is_lowercase() { + xname.push('_'); + } + xname.push_str(&chars[i].to_lowercase().to_string()); + } + xname +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_transform_name() { + assert_eq!(transform_name("One", ""), "one"); + assert_eq!(transform_name("OneTwo", ""), "one_two"); + assert_eq!(transform_name("OneTwoThree", ""), "one_two_three"); + assert_eq!( + transform_name("SshStateInProgress", "SshState"), + "in_progress" + ); + } + + #[test] + #[should_panic(expected = "strip prefix is not good")] + fn test_transform_name_panic() { + assert_eq!(transform_name("SshStateInProgress", "toto"), "in_progress"); + } +} diff --git a/rust/derive/src/lib.rs b/rust/derive/src/lib.rs index a36f19390c..bdf2da2dcb 100644 --- a/rust/derive/src/lib.rs +++ b/rust/derive/src/lib.rs @@ -23,6 +23,7 @@ use proc_macro::TokenStream; mod applayerevent; mod applayerframetype; +mod applayerstate; mod stringenum; /// The `AppLayerEvent` derive macro generates a `AppLayerEvent` trait @@ -52,6 +53,11 @@ pub fn derive_app_layer_frame_type(input: TokenStream) -> TokenStream { applayerframetype::derive_app_layer_frame_type(input) } +#[proc_macro_derive(AppLayerState, attributes(suricata))] +pub fn derive_app_layer_state(input: TokenStream) -> TokenStream { + applayerstate::derive_app_layer_state(input) +} + #[proc_macro_derive(EnumStringU8, attributes(name))] pub fn derive_enum_string_u8(input: TokenStream) -> TokenStream { stringenum::derive_enum_string::(input, "u8") diff --git a/rust/src/applayer.rs b/rust/src/applayer.rs index 4cd36339ad..d09619e2cb 100644 --- a/rust/src/applayer.rs +++ b/rust/src/applayer.rs @@ -18,7 +18,7 @@ //! Parser registration functions and common interface module. use std; -use crate::core::{self,DetectEngineState,AppLayerEventType, GenericVar}; +use crate::core::{self,DetectEngineState,AppLayerEventType, GenericVar, STREAM_TOSERVER}; use crate::direction::Direction; use crate::filecontainer::FileContainer; use crate::flow::Flow; @@ -777,3 +777,83 @@ pub trait AppLayerFrameType { Self::from_u8(id).map(|s| s.to_cstring()).unwrap_or_else(std::ptr::null) } } + +/// AppLayerState trait. +/// +/// This is the behavior expected from an enum of state progress. For most instances +/// this behavior can be derived. This is for protocols which do not need direction, +/// like SSH (which is symmetric). +/// +/// Example: +/// +/// #[derive(AppLayerState)] +/// enum SomeProtoState { +/// Start, +/// Complete, +/// } +pub trait AppLayerState { + /// Create a state progress variant from a u8. + /// + /// None will be returned if there is no matching enum variant. + fn from_u8(value: u8) -> Option + where + Self: Sized; + + /// Return the u8 value of the enum where the first entry has the value of 0. + fn as_u8(&self) -> u8; + + /// Create a state progress variant from a &str. + /// + /// None will be returned if there is no matching enum variant. + fn from_str(s: &str) -> Option + where + Self: Sized; + + /// Return a pointer to a C string of the enum variant suitable as-is for + /// FFI. + fn to_cstring(&self, to_server: bool) -> *const c_char; + + /// Converts a C string formatted name to a state progress. + unsafe extern "C" fn ffi_id_from_name(name: *const c_char, dir: u8) -> c_int + where + Self: Sized, + { + if name.is_null() { + return -1; + } + if let Ok(s) = std::ffi::CStr::from_ptr(name).to_str() { + let dir = Direction::from(dir); + let s2 = match dir { + Direction::ToServer => { + if !s.starts_with("request_") { + return -1; + } + &s["request_".len()..] + } + Direction::ToClient => { + if !s.starts_with("response_") { + return -1; + } + &s["response_".len()..] + } + }; + Self::from_str(s2).map(|t| t.as_u8() as c_int).unwrap_or(-1) + } else { + -1 + } + } + + /// Converts a variant ID to an FFI name. + unsafe extern "C" fn ffi_name_from_id(id: c_int, dir: u8) -> *const c_char + where + Self: Sized, + { + if id < 0 || id > c_int::from(u8::MAX) { + return std::ptr::null(); + } + if let Some(v) = Self::from_u8(id as u8) { + return v.to_cstring(dir == STREAM_TOSERVER); + } + return std::ptr::null(); + } +}