quic-keywords
nfs-keywords
smtp-keywords
+ websocket-keywords
app-layer
xbits
thresholding
* snmp
* tftp
* sip
+* websocket
The availability of these protocols depends on whether the protocol
is enabled in the configuration file, suricata.yaml.
--- /dev/null
+WebSocket Keywords
+==================
+
+websocket.payload
+-----------------
+
+A sticky buffer on the unmasked payload,
+limited by suricata.yaml config value ``websocket.max-payload-size``.
+
+Examples::
+
+ websocket.payload; pcre:"/^123[0-9]*/";
+ websocket.payload content:"swordfish";
+
+``websocket.payload`` is a 'sticky buffer' and can be used as ``fast_pattern``.
+
+websocket.flags
+---------------
+
+Matches on the websocket flags.
+It uses a 8-bit unsigned integer as value.
+Only the four upper bits are used.
+
+The value can also be a list of strings (comma-separated),
+where each string is the name of a specific bit like `fin` and `comp`,
+and can be prefixed by `!` for negation.
+
+websocket.flags uses an :ref:`unsigned 8-bits integer <rules-integer-keywords>`
+
+Examples::
+
+ websocket.flags:128;
+ websocket.flags:&0x40=0x40;
+ websocket.flags:fin,!comp;
+
+websocket.mask
+--------------
+
+Matches on the websocket mask if any.
+It uses a 32-bit unsigned integer as value (big-endian).
+
+websocket.mask uses an :ref:`unsigned 32-bits integer <rules-integer-keywords>`
+
+Examples::
+
+ websocket.mask:123456;
+ websocket.mask:>0;
+
+websocket.opcode
+----------------
+
+Matches on the websocket opcode.
+It uses a 8-bit unsigned integer as value.
+Only 16 values are relevant.
+It can also be specified by text from the enumeration
+
+websocket.opcode uses an :ref:`unsigned 8-bits integer <rules-integer-keywords>`
+
+Examples::
+
+ websocket.opcode:1;
+ websocket.opcode:>8;
+ websocket.opcode:ping;
"tls": {
"description": "Errors encountered parsing TLS protocol",
"$ref": "#/$defs/stats_applayer_error"
+ },
+ "websocket": {
+ "$ref": "#/$defs/stats_applayer_error"
}
},
"additionalProperties": false
"tls": {
"description": "Number of flows for TLS protocol",
"type": "integer"
+ },
+ "websocket": {
+ "type": "integer"
}
},
"additionalProperties": false
},
"tls": {
"type": "integer"
+ },
+ "websocket": {
+ "type": "integer"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
+ },
+ "websocket": {
+ "type": "object",
+ "properties": {
+ "fin": {
+ "type": "boolean"
+ },
+ "mask": {
+ "type": "integer"
+ },
+ "opcode": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
}
},
"$defs": {
smtp-events.rules \
ssh-events.rules \
stream-events.rules \
-tls-events.rules
+tls-events.rules \
+websocket-events.rules
--- /dev/null
+# WebSocket app-layer event rules.
+#
+# These SIDs fall in the 2235000+ range. See:
+# http://doc.emergingthreats.net/bin/view/Main/SidAllocation and
+# https://redmine.openinfosecfoundation.org/projects/suricata/wiki/AppLayer
+
+alert websocket any any -> any any (msg:"SURICATA Websocket skipped end of payload"; app-layer-event:websocket.skip_end_of_payload; classtype:protocol-command-decode; sid:2235000; rev:1;)
+alert websocket any any -> any any (msg:"SURICATA Websocket reassembly limit reached"; app-layer-event:websocket.reassembly_limit_reached; classtype:protocol-command-decode; sid:2235001; rev:1;)
pub mod mqtt;
pub mod pgsql;
pub mod telnet;
+pub mod websocket;
pub mod applayertemplate;
pub mod rdp;
pub mod x509;
--- /dev/null
+/* Copyright (C) 2023 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.
+ */
+
+use super::websocket::WebSocketTransaction;
+use crate::detect::uint::{
+ detect_parse_uint, detect_parse_uint_enum, DetectUintData, DetectUintMode,
+};
+use crate::websocket::parser::WebSocketOpcode;
+
+use nom7::branch::alt;
+use nom7::bytes::complete::{is_a, tag};
+use nom7::combinator::{opt, value};
+use nom7::multi::many1;
+use nom7::IResult;
+
+use std::ffi::CStr;
+
+#[no_mangle]
+pub unsafe extern "C" fn SCWebSocketGetOpcode(tx: &mut WebSocketTransaction) -> u8 {
+ return tx.pdu.opcode;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCWebSocketGetFlags(tx: &mut WebSocketTransaction) -> u8 {
+ return tx.pdu.flags;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCWebSocketGetPayload(
+ tx: &WebSocketTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
+) -> bool {
+ *buffer = tx.pdu.payload.as_ptr();
+ *buffer_len = tx.pdu.payload.len() as u32;
+ return true;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCWebSocketGetMask(
+ tx: &mut WebSocketTransaction, value: *mut u32,
+) -> bool {
+ if let Some(xorkey) = tx.pdu.mask {
+ *value = xorkey;
+ return true;
+ }
+ return false;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCWebSocketParseOpcode(
+ ustr: *const std::os::raw::c_char,
+) -> *mut DetectUintData<u8> {
+ let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe
+ if let Ok(s) = ft_name.to_str() {
+ if let Some(ctx) = detect_parse_uint_enum::<u8, WebSocketOpcode>(s) {
+ let boxed = Box::new(ctx);
+ return Box::into_raw(boxed) as *mut _;
+ }
+ }
+ return std::ptr::null_mut();
+}
+
+struct WebSocketFlag {
+ neg: bool,
+ value: u8,
+}
+
+fn parse_flag_list_item(s: &str) -> IResult<&str, WebSocketFlag> {
+ let (s, _) = opt(is_a(" "))(s)?;
+ let (s, neg) = opt(tag("!"))(s)?;
+ let neg = neg.is_some();
+ let (s, value) = alt((value(0x80, tag("fin")), value(0x40, tag("comp"))))(s)?;
+ let (s, _) = opt(is_a(" ,"))(s)?;
+ Ok((s, WebSocketFlag { neg, value }))
+}
+
+fn parse_flag_list(s: &str) -> IResult<&str, Vec<WebSocketFlag>> {
+ return many1(parse_flag_list_item)(s);
+}
+
+fn parse_flags(s: &str) -> Option<DetectUintData<u8>> {
+ // try first numerical value
+ if let Ok((_, ctx)) = detect_parse_uint::<u8>(s) {
+ return Some(ctx);
+ }
+ // otherwise, try strings for bitmask
+ if let Ok((_, l)) = parse_flag_list(s) {
+ let mut arg1 = 0;
+ let mut arg2 = 0;
+ for elem in l.iter() {
+ if elem.value & arg1 != 0 {
+ SCLogWarning!("Repeated bitflag for websocket.flags");
+ return None;
+ }
+ arg1 |= elem.value;
+ if !elem.neg {
+ arg2 |= elem.value;
+ }
+ }
+ let ctx = DetectUintData::<u8> {
+ arg1,
+ arg2,
+ mode: DetectUintMode::DetectUintModeBitmask,
+ };
+ return Some(ctx);
+ }
+ return None;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCWebSocketParseFlags(
+ ustr: *const std::os::raw::c_char,
+) -> *mut DetectUintData<u8> {
+ let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe
+ if let Ok(s) = ft_name.to_str() {
+ if let Some(ctx) = parse_flags(s) {
+ let boxed = Box::new(ctx);
+ return Box::into_raw(boxed) as *mut _;
+ }
+ }
+ return std::ptr::null_mut();
+}
--- /dev/null
+/* Copyright (C) 2023 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.
+ */
+
+use super::parser::WebSocketOpcode;
+use super::websocket::WebSocketTransaction;
+use crate::detect::EnumString;
+use crate::jsonbuilder::{JsonBuilder, JsonError};
+use std;
+
+fn log_websocket(tx: &WebSocketTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> {
+ js.open_object("websocket")?;
+ js.set_bool("fin", tx.pdu.fin)?;
+ if let Some(xorkey) = tx.pdu.mask {
+ js.set_uint("mask", xorkey.into())?;
+ }
+ if let Some(opcode) = WebSocketOpcode::from_u(tx.pdu.opcode) {
+ js.set_string("opcode", opcode.to_str())?;
+ } else {
+ js.set_string("opcode", &format!("unknown-{}", tx.pdu.opcode))?;
+ }
+ js.close()?;
+ Ok(())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_websocket_logger_log(
+ tx: *mut std::os::raw::c_void, js: &mut JsonBuilder,
+) -> bool {
+ let tx = cast_pointer!(tx, WebSocketTransaction);
+ log_websocket(tx, js).is_ok()
+}
--- /dev/null
+/* Copyright (C) 2023 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.
+ */
+
+//! Application layer websocket parser and logger module.
+
+pub mod detect;
+pub mod logger;
+mod parser;
+pub mod websocket;
--- /dev/null
+/* Copyright (C) 2023 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.
+ */
+
+use nom7::bytes::streaming::take;
+use nom7::combinator::cond;
+use nom7::number::streaming::{be_u16, be_u32, be_u64, be_u8};
+use nom7::IResult;
+use suricata_derive::EnumStringU8;
+
+#[derive(Clone, Debug, Default, EnumStringU8)]
+#[repr(u8)]
+pub enum WebSocketOpcode {
+ #[default]
+ Continuation = 0,
+ Text = 1,
+ Binary = 2,
+ Ping = 8,
+ Pong = 9,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct WebSocketPdu {
+ pub flags: u8,
+ pub fin: bool,
+ pub compress: bool,
+ pub opcode: u8,
+ pub mask: Option<u32>,
+ pub payload: Vec<u8>,
+ pub to_skip: u64,
+}
+
+// cf rfc6455#section-5.2
+pub fn parse_message(i: &[u8], max_pl_size: u32) -> IResult<&[u8], WebSocketPdu> {
+ let (i, flags_op) = be_u8(i)?;
+ let fin = (flags_op & 0x80) != 0;
+ let compress = (flags_op & 0x40) != 0;
+ let flags = flags_op & 0xF0;
+ let opcode = flags_op & 0xF;
+ let (i, mask_plen) = be_u8(i)?;
+ let mask_flag = (mask_plen & 0x80) != 0;
+ let (i, payload_len) = match mask_plen & 0x7F {
+ 126 => {
+ let (i, val) = be_u16(i)?;
+ Ok((i, val.into()))
+ }
+ 127 => be_u64(i),
+ _ => Ok((i, (mask_plen & 0x7F).into())),
+ }?;
+ let (i, xormask) = cond(mask_flag, take(4usize))(i)?;
+ let mask = if mask_flag {
+ let (_, m) = be_u32(xormask.unwrap())?;
+ Some(m)
+ } else {
+ None
+ };
+ // we limit payload_len to u32, so as to build on 32-bit system
+ // where we cannot take(usize) with a u64
+ let (to_skip, payload_len) = if payload_len < max_pl_size.into() {
+ (0, payload_len as u32)
+ } else {
+ (payload_len - (max_pl_size as u64), max_pl_size)
+ };
+ let (i, payload_raw) = take(payload_len)(i)?;
+ let mut payload = payload_raw.to_vec();
+ if let Some(xorkey) = xormask {
+ for i in 0..payload.len() {
+ payload[i] ^= xorkey[i % 4];
+ }
+ }
+ Ok((
+ i,
+ WebSocketPdu {
+ flags,
+ fin,
+ compress,
+ opcode,
+ mask,
+ payload,
+ to_skip,
+ },
+ ))
+}
--- /dev/null
+/* Copyright (C) 2023 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.
+ */
+
+use super::parser;
+use crate::applayer::{self, *};
+use crate::conf::conf_get;
+use crate::core::{AppProto, Direction, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_TCP};
+use crate::frames::Frame;
+
+use nom7 as nom;
+use nom7::Needed;
+
+use flate2::read::DeflateDecoder;
+
+use std;
+use std::collections::VecDeque;
+use std::ffi::CString;
+use std::io::Read;
+use std::os::raw::{c_char, c_int, c_void};
+
+static mut ALPROTO_WEBSOCKET: AppProto = ALPROTO_UNKNOWN;
+
+static mut WEBSOCKET_MAX_PAYLOAD_SIZE: u32 = 0xFFFF;
+
+// app-layer-frame-documentation tag start: FrameType enum
+#[derive(AppLayerFrameType)]
+pub enum WebSocketFrameType {
+ Header,
+ Pdu,
+}
+
+#[derive(AppLayerEvent)]
+pub enum WebSocketEvent {
+ SkipEndOfPayload,
+ ReassemblyLimitReached,
+}
+
+#[derive(Default)]
+pub struct WebSocketTransaction {
+ tx_id: u64,
+ pub pdu: parser::WebSocketPdu,
+ tx_data: AppLayerTxData,
+}
+
+impl WebSocketTransaction {
+ pub fn new(direction: Direction) -> WebSocketTransaction {
+ Self {
+ tx_data: AppLayerTxData::for_direction(direction),
+ ..Default::default()
+ }
+ }
+}
+
+impl Transaction for WebSocketTransaction {
+ fn id(&self) -> u64 {
+ self.tx_id
+ }
+}
+
+#[derive(Default)]
+struct WebSocketReassemblyBuffer {
+ data: Vec<u8>,
+ compress: bool,
+}
+
+#[derive(Default)]
+pub struct WebSocketState {
+ state_data: AppLayerStateData,
+ tx_id: u64,
+ transactions: VecDeque<WebSocketTransaction>,
+
+ c2s_buf: WebSocketReassemblyBuffer,
+ s2c_buf: WebSocketReassemblyBuffer,
+
+ to_skip_tc: u64,
+ to_skip_ts: u64,
+}
+
+impl State<WebSocketTransaction> for WebSocketState {
+ fn get_transaction_count(&self) -> usize {
+ self.transactions.len()
+ }
+
+ fn get_transaction_by_index(&self, index: usize) -> Option<&WebSocketTransaction> {
+ self.transactions.get(index)
+ }
+}
+
+impl WebSocketState {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ // Free a transaction by ID.
+ fn free_tx(&mut self, tx_id: u64) {
+ let len = self.transactions.len();
+ let mut found = false;
+ let mut index = 0;
+ for i in 0..len {
+ let tx = &self.transactions[i];
+ if tx.tx_id == tx_id + 1 {
+ found = true;
+ index = i;
+ break;
+ }
+ }
+ if found {
+ self.transactions.remove(index);
+ }
+ }
+
+ pub fn get_tx(&mut self, tx_id: u64) -> Option<&WebSocketTransaction> {
+ self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1)
+ }
+
+ fn new_tx(&mut self, direction: Direction) -> WebSocketTransaction {
+ let mut tx = WebSocketTransaction::new(direction);
+ self.tx_id += 1;
+ tx.tx_id = self.tx_id;
+ return tx;
+ }
+
+ fn parse(
+ &mut self, stream_slice: StreamSlice, direction: Direction, flow: *const Flow,
+ ) -> AppLayerResult {
+ let to_skip = if direction == Direction::ToClient {
+ &mut self.to_skip_tc
+ } else {
+ &mut self.to_skip_ts
+ };
+ let input = stream_slice.as_slice();
+ let mut start = input;
+ if *to_skip > 0 {
+ if *to_skip >= input.len() as u64 {
+ *to_skip -= input.len() as u64;
+ return AppLayerResult::ok();
+ } else {
+ start = &input[*to_skip as usize..];
+ *to_skip = 0;
+ }
+ }
+
+ let max_pl_size = unsafe { WEBSOCKET_MAX_PAYLOAD_SIZE };
+ while !start.is_empty() {
+ match parser::parse_message(start, max_pl_size) {
+ Ok((rem, pdu)) => {
+ let _pdu = Frame::new(
+ flow,
+ &stream_slice,
+ start,
+ (start.len() - rem.len() - pdu.payload.len()) as i64,
+ WebSocketFrameType::Header as u8,
+ );
+ let _pdu = Frame::new(
+ flow,
+ &stream_slice,
+ start,
+ (start.len() - rem.len()) as i64,
+ WebSocketFrameType::Pdu as u8,
+ );
+ start = rem;
+ let mut tx = self.new_tx(direction);
+ if pdu.to_skip > 0 {
+ if direction == Direction::ToClient {
+ self.to_skip_tc = pdu.to_skip;
+ } else {
+ self.to_skip_ts = pdu.to_skip;
+ }
+ tx.tx_data.set_event(WebSocketEvent::SkipEndOfPayload as u8);
+ }
+ let buf = if direction == Direction::ToClient {
+ &mut self.s2c_buf
+ } else {
+ &mut self.c2s_buf
+ };
+ if !buf.data.is_empty() || !pdu.fin {
+ if buf.data.is_empty() {
+ buf.compress = pdu.compress;
+ }
+ if buf.data.len() + pdu.payload.len() < max_pl_size as usize {
+ buf.data.extend(&pdu.payload);
+ } else if buf.data.len() < max_pl_size as usize {
+ buf.data
+ .extend(&pdu.payload[..max_pl_size as usize - buf.data.len()]);
+ tx.tx_data
+ .set_event(WebSocketEvent::ReassemblyLimitReached as u8);
+ }
+ }
+ tx.pdu = pdu;
+ if tx.pdu.fin && !buf.data.is_empty() {
+ // the final PDU gets the full reassembled payload
+ std::mem::swap(&mut tx.pdu.payload, &mut buf.data);
+ buf.data.clear();
+ }
+ if buf.compress && tx.pdu.fin {
+ buf.compress = false;
+ // cf RFC 7692 section-7.2.2
+ tx.pdu.payload.extend_from_slice(&[0, 0, 0xFF, 0xFF]);
+ let mut deflater = DeflateDecoder::new(&tx.pdu.payload[..]);
+ let mut v = Vec::new();
+ // do not check result because
+ // deflate with rust backend fails on good input cf https://github.com/rust-lang/flate2-rs/issues/389
+ let _ = deflater.read_to_end(&mut v);
+ if !v.is_empty() {
+ std::mem::swap(&mut tx.pdu.payload, &mut v);
+ }
+ }
+ self.transactions.push_back(tx);
+ }
+ Err(nom::Err::Incomplete(needed)) => {
+ if let Needed::Size(n) = needed {
+ let n = usize::from(n);
+ // Not enough data. just ask for one more byte.
+ let consumed = input.len() - start.len();
+ let needed = start.len() + n;
+ return AppLayerResult::incomplete(consumed as u32, needed as u32);
+ }
+ return AppLayerResult::err();
+ }
+ Err(_) => {
+ return AppLayerResult::err();
+ }
+ }
+ }
+ // Input was fully consumed.
+ return AppLayerResult::ok();
+ }
+}
+
+// C exports.
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_websocket_probing_parser(
+ _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8,
+) -> AppProto {
+ if !input.is_null() {
+ let slice = build_slice!(input, input_len as usize);
+ if !slice.is_empty() {
+ // just check reserved bits are zeroed, except RSV1
+ // as RSV1 is used for compression cf RFC 7692
+ if slice[0] & 0x30 == 0 {
+ return ALPROTO_WEBSOCKET;
+ }
+ return ALPROTO_FAILED;
+ }
+ }
+ return ALPROTO_UNKNOWN;
+}
+
+extern "C" fn rs_websocket_state_new(
+ _orig_state: *mut c_void, _orig_proto: AppProto,
+) -> *mut c_void {
+ let state = WebSocketState::new();
+ let boxed = Box::new(state);
+ return Box::into_raw(boxed) as *mut c_void;
+}
+
+unsafe extern "C" fn rs_websocket_state_free(state: *mut c_void) {
+ std::mem::drop(Box::from_raw(state as *mut WebSocketState));
+}
+
+unsafe extern "C" fn rs_websocket_state_tx_free(state: *mut c_void, tx_id: u64) {
+ let state = cast_pointer!(state, WebSocketState);
+ state.free_tx(tx_id);
+}
+
+unsafe extern "C" fn rs_websocket_parse_request(
+ flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice,
+ _data: *const c_void,
+) -> AppLayerResult {
+ let state = cast_pointer!(state, WebSocketState);
+ state.parse(stream_slice, Direction::ToServer, flow)
+}
+
+unsafe extern "C" fn rs_websocket_parse_response(
+ flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice,
+ _data: *const c_void,
+) -> AppLayerResult {
+ let state = cast_pointer!(state, WebSocketState);
+ state.parse(stream_slice, Direction::ToClient, flow)
+}
+
+unsafe extern "C" fn rs_websocket_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void {
+ let state = cast_pointer!(state, WebSocketState);
+ match state.get_tx(tx_id) {
+ Some(tx) => {
+ return tx as *const _ as *mut _;
+ }
+ None => {
+ return std::ptr::null_mut();
+ }
+ }
+}
+
+unsafe extern "C" fn rs_websocket_state_get_tx_count(state: *mut c_void) -> u64 {
+ let state = cast_pointer!(state, WebSocketState);
+ return state.tx_id;
+}
+
+unsafe extern "C" fn rs_websocket_tx_get_alstate_progress(
+ _tx: *mut c_void, _direction: u8,
+) -> c_int {
+ return 1;
+}
+
+export_tx_data_get!(rs_websocket_get_tx_data, WebSocketTransaction);
+export_state_data_get!(rs_websocket_get_state_data, WebSocketState);
+
+// Parser name as a C style string.
+const PARSER_NAME: &[u8] = b"websocket\0";
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_websocket_register_parser() {
+ let parser = RustParser {
+ name: PARSER_NAME.as_ptr() as *const c_char,
+ default_port: std::ptr::null(),
+ ipproto: IPPROTO_TCP,
+ probe_ts: Some(rs_websocket_probing_parser),
+ probe_tc: Some(rs_websocket_probing_parser),
+ min_depth: 0,
+ max_depth: 16,
+ state_new: rs_websocket_state_new,
+ state_free: rs_websocket_state_free,
+ tx_free: rs_websocket_state_tx_free,
+ parse_ts: rs_websocket_parse_request,
+ parse_tc: rs_websocket_parse_response,
+ get_tx_count: rs_websocket_state_get_tx_count,
+ get_tx: rs_websocket_state_get_tx,
+ tx_comp_st_ts: 1,
+ tx_comp_st_tc: 1,
+ tx_get_progress: rs_websocket_tx_get_alstate_progress,
+ get_eventinfo: Some(WebSocketEvent::get_event_info),
+ get_eventinfo_byid: Some(WebSocketEvent::get_event_info_by_id),
+ localstorage_new: None,
+ localstorage_free: None,
+ get_tx_files: None,
+ get_tx_iterator: Some(
+ applayer::state_get_tx_iterator::<WebSocketState, WebSocketTransaction>,
+ ),
+ get_tx_data: rs_websocket_get_tx_data,
+ get_state_data: rs_websocket_get_state_data,
+ apply_tx_config: None,
+ flags: 0, // do not accept gaps as there is no good way to resync
+ truncate: None,
+ get_frame_id_by_name: Some(WebSocketFrameType::ffi_id_from_name),
+ get_frame_name_by_id: Some(WebSocketFrameType::ffi_name_from_id),
+ };
+
+ let ip_proto_str = CString::new("tcp").unwrap();
+
+ if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
+ let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
+ ALPROTO_WEBSOCKET = alproto;
+ if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
+ let _ = AppLayerRegisterParser(&parser, alproto);
+ }
+ SCLogDebug!("Rust websocket parser registered.");
+ if let Some(val) = conf_get("app-layer.protocols.websocket.max-payload-size") {
+ if let Ok(v) = val.parse::<u32>() {
+ WEBSOCKET_MAX_PAYLOAD_SIZE = v;
+ } else {
+ SCLogError!("Invalid value for websocket.max-payload-size");
+ }
+ }
+ AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_WEBSOCKET);
+ } else {
+ SCLogDebug!("Protocol detector and parser disabled for WEBSOCKET.");
+ }
+}
detect-urilen.h \
detect-within.h \
detect-xbits.h \
+ detect-websocket.h \
device-storage.h \
feature.h \
flow-bit.h \
detect-urilen.c \
detect-within.c \
detect-xbits.c \
+ detect-websocket.c \
device-storage.c \
feature.c \
flow-bit.c \
#include "app-layer-protos.h"
#include "app-layer-parser.h"
+#include "app-layer-expectation.h"
#include "app-layer.h"
#include "app-layer-detect-proto.h"
if (tx != NULL && tx->response_status_number == 101) {
htp_header_t *h =
(htp_header_t *)htp_table_get_c(tx->response_headers, "Upgrade");
- if (h == NULL || bstr_cmp_c(h->value, "h2c") != 0) {
- break;
- }
- if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) {
- // if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode
+ if (h == NULL) {
break;
}
uint16_t dp = 0;
dp = (uint16_t)tx->request_port_number;
}
consumed = htp_connp_res_data_consumed(hstate->connp);
- hstate->slice = NULL;
- if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) {
- HTPSetEvent(hstate, NULL, STREAM_TOCLIENT,
- HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE);
- }
- // During HTTP2 upgrade, we may consume the HTTP1 part of the data
- // and we need to parser the remaining part with HTTP2
- if (consumed > 0 && consumed < input_len) {
- SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed));
+ if (bstr_cmp_c(h->value, "h2c") == 0) {
+ if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) {
+ // if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode
+ break;
+ }
+ hstate->slice = NULL;
+ if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) {
+ HTPSetEvent(hstate, NULL, STREAM_TOCLIENT,
+ HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE);
+ }
+ // During HTTP2 upgrade, we may consume the HTTP1 part of the data
+ // and we need to parser the remaining part with HTTP2
+ if (consumed > 0 && consumed < input_len) {
+ SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed));
+ }
+ SCReturnStruct(APP_LAYER_OK);
+ } else if (bstr_cmp_c_nocase(h->value, "WebSocket") == 0) {
+ if (AppLayerProtoDetectGetProtoName(ALPROTO_WEBSOCKET) == NULL) {
+ // if WS is disabled, keep the HTP_STREAM_TUNNEL mode
+ break;
+ }
+ hstate->slice = NULL;
+ if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_WEBSOCKET)) {
+ HTPSetEvent(hstate, NULL, STREAM_TOCLIENT,
+ HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE);
+ }
+ // During WS upgrade, we may consume the HTTP1 part of the data
+ // and we need to parser the remaining part with WS
+ if (consumed > 0 && consumed < input_len) {
+ SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed));
+ }
+ SCReturnStruct(APP_LAYER_OK);
}
- SCReturnStruct(APP_LAYER_OK);
}
break;
default:
RegisterSNMPParsers();
RegisterSIPParsers();
RegisterQuicParsers();
+ rs_websocket_register_parser();
rs_template_register_parser();
RegisterRFBParsers();
SCMqttRegisterParser();
{ ALPROTO_MQTT, "mqtt" },
{ ALPROTO_PGSQL, "pgsql" },
{ ALPROTO_TELNET, "telnet" },
+ { ALPROTO_WEBSOCKET, "websocket" },
{ ALPROTO_TEMPLATE, "template" },
{ ALPROTO_RDP, "rdp" },
{ ALPROTO_HTTP2, "http2" },
ALPROTO_MQTT,
ALPROTO_PGSQL,
ALPROTO_TELNET,
+ ALPROTO_WEBSOCKET,
ALPROTO_TEMPLATE,
ALPROTO_RDP,
ALPROTO_HTTP2,
#include "detect-quic-cyu-hash.h"
#include "detect-quic-cyu-string.h"
#include "detect-ja4-hash.h"
+#include "detect-websocket.h"
#include "detect-bypass.h"
#include "detect-ftpdata.h"
DetectQuicCyuHashRegister();
DetectQuicCyuStringRegister();
DetectJa4HashRegister();
+ DetectWebsocketRegister();
DetectBypassRegister();
DetectConfigRegister();
DETECT_AL_QUIC_UA,
DETECT_AL_QUIC_CYU_HASH,
DETECT_AL_QUIC_CYU_STRING,
+ DETECT_WEBSOCKET_MASK,
+ DETECT_WEBSOCKET_OPCODE,
+ DETECT_WEBSOCKET_FLAGS,
+ DETECT_WEBSOCKET_PAYLOAD,
DETECT_BYPASS,
--- /dev/null
+/* Copyright (C) 2023 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.
+ */
+
+/**
+ * \file
+ *
+ * \author Philippe Antoine
+ */
+
+#include "suricata-common.h"
+#include "detect.h"
+#include "detect-parse.h"
+#include "detect-engine.h"
+#include "detect-engine-content-inspection.h"
+#include "detect-engine-uint.h"
+#include "detect-engine-prefilter.h"
+#include "detect-websocket.h"
+
+#include "rust.h"
+
+static int websocket_tx_id = 0;
+static int websocket_payload_id = 0;
+
+/**
+ * \internal
+ * \brief this function will free memory associated with DetectWebSocketOpcodeData
+ *
+ * \param de pointer to DetectWebSocketOpcodeData
+ */
+static void DetectWebSocketOpcodeFree(DetectEngineCtx *de_ctx, void *de_ptr)
+{
+ rs_detect_u8_free(de_ptr);
+}
+
+/**
+ * \internal
+ * \brief Function to match opcode of a websocket tx
+ *
+ * \param det_ctx Pointer to the pattern matcher thread.
+ * \param f Pointer to the current flow.
+ * \param flags Flags.
+ * \param state App layer state.
+ * \param txv Pointer to the transaction.
+ * \param s Pointer to the Signature.
+ * \param ctx Pointer to the sigmatch that we will cast into DetectWebSocketOpcodeData.
+ *
+ * \retval 0 no match.
+ * \retval 1 match.
+ */
+static int DetectWebSocketOpcodeMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags,
+ void *state, void *txv, const Signature *s, const SigMatchCtx *ctx)
+{
+ const DetectU8Data *de = (const DetectU8Data *)ctx;
+ uint8_t opc = SCWebSocketGetOpcode(txv);
+ return DetectU8Match(opc, de);
+}
+
+/**
+ * \internal
+ * \brief this function is used to add the parsed sigmatch into the current signature
+ *
+ * \param de_ctx pointer to the Detection Engine Context
+ * \param s pointer to the Current Signature
+ * \param rawstr pointer to the user provided options
+ *
+ * \retval 0 on Success
+ * \retval -1 on Failure
+ */
+static int DetectWebSocketOpcodeSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr)
+{
+ if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0)
+ return -1;
+
+ DetectU8Data *de = SCWebSocketParseOpcode(rawstr);
+ if (de == NULL)
+ return -1;
+
+ if (SigMatchAppendSMToList(
+ de_ctx, s, DETECT_WEBSOCKET_OPCODE, (SigMatchCtx *)de, websocket_tx_id) == NULL) {
+ DetectWebSocketOpcodeFree(de_ctx, de);
+ return -1;
+ }
+
+ return 0;
+}
+
+/**
+ * \internal
+ * \brief this function will free memory associated with DetectWebSocketMaskData
+ *
+ * \param de pointer to DetectWebSocketMaskData
+ */
+static void DetectWebSocketMaskFree(DetectEngineCtx *de_ctx, void *de_ptr)
+{
+ rs_detect_u32_free(de_ptr);
+}
+
+static int DetectWebSocketMaskMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags,
+ void *state, void *txv, const Signature *s, const SigMatchCtx *ctx)
+{
+ uint32_t val;
+ const DetectU32Data *du32 = (const DetectU32Data *)ctx;
+ if (SCWebSocketGetMask(txv, &val)) {
+ return DetectU32Match(val, du32);
+ }
+ return 0;
+}
+
+static int DetectWebSocketMaskSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr)
+{
+ if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0)
+ return -1;
+
+ DetectU32Data *du32 = DetectU32Parse(rawstr);
+ if (du32 == NULL)
+ return -1;
+
+ if (SigMatchAppendSMToList(
+ de_ctx, s, DETECT_WEBSOCKET_MASK, (SigMatchCtx *)du32, websocket_tx_id) == NULL) {
+ DetectWebSocketMaskFree(de_ctx, du32);
+ return -1;
+ }
+
+ return 0;
+}
+
+static void DetectWebSocketFlagsFree(DetectEngineCtx *de_ctx, void *de_ptr)
+{
+ rs_detect_u8_free(de_ptr);
+}
+
+static int DetectWebSocketFlagsMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags,
+ void *state, void *txv, const Signature *s, const SigMatchCtx *ctx)
+{
+ const DetectU8Data *de = (const DetectU8Data *)ctx;
+ uint8_t val = SCWebSocketGetFlags(txv);
+ return DetectU8Match(val, de);
+}
+
+static int DetectWebSocketFlagsSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr)
+{
+ if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0)
+ return -1;
+
+ DetectU8Data *de = SCWebSocketParseFlags(rawstr);
+ if (de == NULL)
+ return -1;
+
+ if (SigMatchAppendSMToList(
+ de_ctx, s, DETECT_WEBSOCKET_FLAGS, (SigMatchCtx *)de, websocket_tx_id) == NULL) {
+ DetectWebSocketOpcodeFree(de_ctx, de);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int DetectWebSocketPayloadSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rulestr)
+{
+ if (DetectBufferSetActiveList(de_ctx, s, websocket_payload_id) < 0)
+ return -1;
+
+ if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) != 0)
+ return -1;
+
+ return 0;
+}
+
+static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx,
+ const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv,
+ const int list_id)
+{
+ InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id);
+ if (buffer->inspect == NULL) {
+ const uint8_t *b = NULL;
+ uint32_t b_len = 0;
+
+ if (!SCWebSocketGetPayload(txv, &b, &b_len))
+ return NULL;
+ if (b == NULL || b_len == 0)
+ return NULL;
+
+ InspectionBufferSetup(det_ctx, list_id, buffer, b, b_len);
+ InspectionBufferApplyTransforms(buffer, transforms);
+ }
+ return buffer;
+}
+
+/**
+ * \brief Registration function for websocket.opcode: keyword
+ */
+void DetectWebsocketRegister(void)
+{
+ sigmatch_table[DETECT_WEBSOCKET_OPCODE].name = "websocket.opcode";
+ sigmatch_table[DETECT_WEBSOCKET_OPCODE].desc = "match WebSocket opcode";
+ sigmatch_table[DETECT_WEBSOCKET_OPCODE].url = "/rules/websocket-keywords.html#websocket-opcode";
+ sigmatch_table[DETECT_WEBSOCKET_OPCODE].AppLayerTxMatch = DetectWebSocketOpcodeMatch;
+ sigmatch_table[DETECT_WEBSOCKET_OPCODE].Setup = DetectWebSocketOpcodeSetup;
+ sigmatch_table[DETECT_WEBSOCKET_OPCODE].Free = DetectWebSocketOpcodeFree;
+
+ DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER, 1,
+ DetectEngineInspectGenericList, NULL);
+ DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT, 1,
+ DetectEngineInspectGenericList, NULL);
+
+ websocket_tx_id = DetectBufferTypeGetByName("websocket.tx");
+
+ sigmatch_table[DETECT_WEBSOCKET_MASK].name = "websocket.mask";
+ sigmatch_table[DETECT_WEBSOCKET_MASK].desc = "match WebSocket mask";
+ sigmatch_table[DETECT_WEBSOCKET_MASK].url = "/rules/websocket-keywords.html#websocket-mask";
+ sigmatch_table[DETECT_WEBSOCKET_MASK].AppLayerTxMatch = DetectWebSocketMaskMatch;
+ sigmatch_table[DETECT_WEBSOCKET_MASK].Setup = DetectWebSocketMaskSetup;
+ sigmatch_table[DETECT_WEBSOCKET_MASK].Free = DetectWebSocketMaskFree;
+
+ sigmatch_table[DETECT_WEBSOCKET_FLAGS].name = "websocket.flags";
+ sigmatch_table[DETECT_WEBSOCKET_FLAGS].desc = "match WebSocket flags";
+ sigmatch_table[DETECT_WEBSOCKET_FLAGS].url = "/rules/websocket-keywords.html#websocket-flags";
+ sigmatch_table[DETECT_WEBSOCKET_FLAGS].AppLayerTxMatch = DetectWebSocketFlagsMatch;
+ sigmatch_table[DETECT_WEBSOCKET_FLAGS].Setup = DetectWebSocketFlagsSetup;
+ sigmatch_table[DETECT_WEBSOCKET_FLAGS].Free = DetectWebSocketFlagsFree;
+
+ sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].name = "websocket.payload";
+ sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].desc = "match WebSocket payload";
+ sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].url =
+ "/rules/websocket-keywords.html#websocket-payload";
+ sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].Setup = DetectWebSocketPayloadSetup;
+ sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].flags |= SIGMATCH_NOOPT;
+ DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER,
+ 0, DetectEngineInspectBufferGeneric, GetData);
+ DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT,
+ 0, DetectEngineInspectBufferGeneric, GetData);
+ DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOSERVER, 2,
+ PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1);
+ DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOCLIENT, 2,
+ PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1);
+ websocket_payload_id = DetectBufferTypeGetByName("websocket.payload");
+}
--- /dev/null
+/* Copyright (C) 2023 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.
+ */
+
+/**
+ * \file
+ *
+ * \author Philippe Antoine
+ */
+
+#ifndef __DETECT_WEBSOCKET_H__
+#define __DETECT_WEBSOCKET_H__
+
+void DetectWebsocketRegister(void);
+
+#endif /* __DETECT_WEBSOCKET_H__ */
JsonMQTTLogRegister();
/* Pgsql JSON logger. */
JsonPgsqlLogRegister();
+ /* WebSocket JSON logger. */
+ OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonWebSocketLog", "eve-log.websocket",
+ OutputJsonLogInitSub, ALPROTO_WEBSOCKET, JsonGenericDirPacketLogger, JsonLogThreadInit,
+ JsonLogThreadDeinit, NULL);
/* Template JSON logger. */
OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template",
OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit,
{ ALPROTO_MQTT, JsonMQTTAddMetadata },
{ ALPROTO_PGSQL, JsonPgsqlAddMetadata },
{ ALPROTO_TELNET, NULL }, // no logging
+ { ALPROTO_WEBSOCKET, rs_websocket_logger_log },
{ ALPROTO_TEMPLATE, rs_template_logger_log },
{ ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json },
{ ALPROTO_HTTP2, rs_http2_log_json },
#md5: [body, subject]
#- dnp3
+ - websocket
- ftp
- rdp
- nfs
ftp:
enabled: yes
# memcap: 64mb
+ websocket:
+ #enabled: yes
+ # Maximum used payload size, the rest is skipped
+ # max-payload-size: 65535
rdp:
#enabled: yes
ssh: