]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
rust: derive for AppLayerState
authorPhilippe Antoine <pantoine@oisf.net>
Tue, 15 Apr 2025 12:12:19 +0000 (14:12 +0200)
committerVictor Julien <victor@inliniac.net>
Fri, 25 Apr 2025 07:51:48 +0000 (09:51 +0200)
To enable easily hooks for rust app-layers such as SSH

rust/derive/src/applayerstate.rs [new file with mode: 0644]
rust/derive/src/lib.rs
rust/src/applayer.rs

diff --git a/rust/derive/src/applayerstate.rs b/rust/derive/src/applayerstate.rs
new file mode 100644 (file)
index 0000000..58e3ceb
--- /dev/null
@@ -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<Self> {
+                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<char> = 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");
+    }
+}
index a36f19390c0c3d4e6ab8b5b9f5c3cf67f94aa38d..bdf2da2dcb2d683c6a93395dd32ff5d179a732ea 100644 (file)
@@ -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::<u8>(input, "u8")
index 4cd36339ad4c222c82bd5e19195e2e8c59535bdb..d09619e2cba63dd11e929e07e76d70022c0f1452 100644 (file)
@@ -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<Self>
+    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<Self>
+    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();
+    }
+}