From 17f6ef052a3f7a77ca8cac2809935d9410e1764c Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Wed, 6 Dec 2023 17:33:02 +0100 Subject: [PATCH] app-layer: websockets protocol support Ticket: 2695 --- etc/schema.json | 22 +++ rust/src/applayer.rs | 1 + rust/src/lib.rs | 1 + rust/src/websockets/logger.rs | 54 ++++++ rust/src/websockets/mod.rs | 22 +++ rust/src/websockets/parser.rs | 63 +++++++ rust/src/websockets/websockets.rs | 268 ++++++++++++++++++++++++++++++ src/Makefile.am | 2 + src/app-layer-htp.c | 54 ++++-- src/app-layer-parser.c | 1 + src/app-layer-protos.c | 1 + src/app-layer-protos.h | 1 + src/output-json-websockets.c | 160 ++++++++++++++++++ src/output-json-websockets.h | 29 ++++ src/output.c | 4 + suricata.yaml.in | 1 + 16 files changed, 669 insertions(+), 15 deletions(-) create mode 100644 rust/src/websockets/logger.rs create mode 100644 rust/src/websockets/mod.rs create mode 100644 rust/src/websockets/parser.rs create mode 100644 rust/src/websockets/websockets.rs create mode 100644 src/output-json-websockets.c create mode 100644 src/output-json-websockets.h diff --git a/etc/schema.json b/etc/schema.json index c194017ddf6f..63b4701a8094 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -3823,6 +3823,9 @@ }, "tls": { "$ref": "#/$defs/stats_applayer_error" + }, + "websockets": { + "$ref": "#/$defs/stats_applayer_error" } }, "additionalProperties": false @@ -3940,6 +3943,9 @@ }, "tls": { "type": "integer" + }, + "websockets": { + "type": "integer" } }, "additionalProperties": false @@ -4051,6 +4057,9 @@ }, "tls": { "type": "integer" + }, + "websockets": { + "type": "integer" } }, "additionalProperties": false @@ -5488,7 +5497,20 @@ } }, "additionalProperties": false + }, + "websockets": { + "type": "object", + "properties": { + "mask": { + "type": "boolean" + }, + "opcode": { + "type": "string" + } + }, + "additionalProperties": false } + }, "$defs": { "stats_applayer_error": { diff --git a/rust/src/applayer.rs b/rust/src/applayer.rs index 97db321e2249..84f00e910cbf 100644 --- a/rust/src/applayer.rs +++ b/rust/src/applayer.rs @@ -449,6 +449,7 @@ pub unsafe fn AppLayerRegisterParser(parser: *const RustParser, alproto: AppProt // Defined in app-layer-detect-proto.h extern { + pub fn AppLayerRegisterExpectationProto(ipproto: u8, alproto: AppProto); pub fn AppLayerProtoDetectPPRegister(ipproto: u8, portstr: *const c_char, alproto: AppProto, min_depth: u16, max_depth: u16, dir: u8, pparser1: ProbeFn, pparser2: ProbeFn); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index da2859637783..7e797583c282 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -103,6 +103,7 @@ pub mod rfb; pub mod mqtt; pub mod pgsql; pub mod telnet; +pub mod websockets; pub mod applayertemplate; pub mod rdp; pub mod x509; diff --git a/rust/src/websockets/logger.rs b/rust/src/websockets/logger.rs new file mode 100644 index 000000000000..090a98a4d5a2 --- /dev/null +++ b/rust/src/websockets/logger.rs @@ -0,0 +1,54 @@ +/* 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::websockets::WebSocketsTransaction; +use crate::jsonbuilder::{JsonBuilder, JsonError}; +use std; + +//TODOws detection on opcode and mask, and payload buffer + +fn ws_opcode_string(p: u8) -> Option<&'static str> { + match p { + 0 => Some("continuation"), + 1 => Some("text"), + 2 => Some("binary"), + 8 => Some("connection_close"), + 9 => Some("ping"), + 0xa => Some("pong"), + _ => None, + } +} + +fn log_websockets(tx: &WebSocketsTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> { + js.open_object("websockets")?; + js.set_bool("mask", tx.pdu.mask)?; + if let Some(val) = ws_opcode_string(tx.pdu.opcode) { + js.set_string("opcode", val)?; + } else { + js.set_string("opcode", &format!("unknown-{}", tx.pdu.opcode))?; + } + js.close()?; + Ok(()) +} + +#[no_mangle] +pub unsafe extern "C" fn rs_websockets_logger_log( + tx: *mut std::os::raw::c_void, js: &mut JsonBuilder, +) -> bool { + let tx = cast_pointer!(tx, WebSocketsTransaction); + log_websockets(tx, js).is_ok() +} diff --git a/rust/src/websockets/mod.rs b/rust/src/websockets/mod.rs new file mode 100644 index 000000000000..7a4cc7caf22c --- /dev/null +++ b/rust/src/websockets/mod.rs @@ -0,0 +1,22 @@ +/* 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 websockets parser and logger module. + +pub mod logger; +mod parser; +pub mod websockets; diff --git a/rust/src/websockets/parser.rs b/rust/src/websockets/parser.rs new file mode 100644 index 000000000000..a25e700448e4 --- /dev/null +++ b/rust/src/websockets/parser.rs @@ -0,0 +1,63 @@ +/* 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_u64, be_u8}; +use nom7::IResult; + +#[derive(Clone, Debug, Default)] +pub struct WebSocketsPdu { + pub fin: bool, + pub opcode: u8, + pub mask: bool, + pub payload: Vec, +} + +// cf rfc6455#section-5.2 +pub fn parse_message(i: &[u8]) -> IResult<&[u8], WebSocketsPdu> { + let (i, fin_op) = be_u8(i)?; + let fin = (fin_op & 0x80) != 0; + let opcode = fin_op & 0xF; + let (i, mask_plen) = be_u8(i)?; + let mask = (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, take(4usize))(i)?; + 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, + WebSocketsPdu { + fin, + opcode, + mask, + payload, + }, + )) +} diff --git a/rust/src/websockets/websockets.rs b/rust/src/websockets/websockets.rs new file mode 100644 index 000000000000..49de9e9cf0f6 --- /dev/null +++ b/rust/src/websockets/websockets.rs @@ -0,0 +1,268 @@ +/* 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::core::{AppProto, Direction, Flow, ALPROTO_UNKNOWN, IPPROTO_TCP}; +use crate::frames::Frame; +use nom7 as nom; +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::os::raw::{c_char, c_int, c_void}; + +static mut ALPROTO_WEBSOCKETS: AppProto = ALPROTO_UNKNOWN; + +// app-layer-frame-documentation tag start: FrameType enum +#[derive(AppLayerFrameType)] +pub enum WebSocketsFrameType { + Header, + Pdu, +} + +#[derive(Default)] +pub struct WebSocketsTransaction { + tx_id: u64, + pub pdu: parser::WebSocketsPdu, + tx_data: AppLayerTxData, +} + +impl WebSocketsTransaction { + pub fn new(direction: Direction) -> WebSocketsTransaction { + Self { + tx_data: AppLayerTxData::for_direction(direction), + ..Default::default() + } + } +} + +impl Transaction for WebSocketsTransaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +#[derive(Default)] +pub struct WebSocketsState { + state_data: AppLayerStateData, + tx_id: u64, + transactions: VecDeque, +} + +impl State for WebSocketsState { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&WebSocketsTransaction> { + self.transactions.get(index) + } +} + +impl WebSocketsState { + 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<&WebSocketsTransaction> { + self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1) + } + + fn new_tx(&mut self, direction: Direction) -> WebSocketsTransaction { + let mut tx = WebSocketsTransaction::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 input = stream_slice.as_slice(); + let mut start = input; + while !start.is_empty() { + match parser::parse_message(start) { + Ok((rem, pdu)) => { + let _pdu = Frame::new( + flow, + &stream_slice, + start, + (start.len() - rem.len() - pdu.payload.len()) as i64, + WebSocketsFrameType::Header as u8, + ); + let _pdu = Frame::new( + flow, + &stream_slice, + start, + (start.len() - rem.len()) as i64, + WebSocketsFrameType::Pdu as u8, + ); + start = rem; + let mut tx = self.new_tx(direction); + tx.pdu = pdu; + //TODOws should we reassemble/stream payload data ? + self.transactions.push_back(tx); + } + Err(nom::Err::Incomplete(_)) => { + // Not enough data. just ask for one more byte. + let consumed = input.len() - start.len(); + let needed = start.len() + 1; + return AppLayerResult::incomplete(consumed as u32, needed as u32); + } + Err(_) => { + return AppLayerResult::err(); + } + } + } + // Input was fully consumed. + return AppLayerResult::ok(); + } +} + +// C exports. + +extern "C" fn rs_websockets_state_new( + _orig_state: *mut c_void, _orig_proto: AppProto, +) -> *mut c_void { + let state = WebSocketsState::new(); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut c_void; +} + +unsafe extern "C" fn rs_websockets_state_free(state: *mut c_void) { + std::mem::drop(Box::from_raw(state as *mut WebSocketsState)); +} + +unsafe extern "C" fn rs_websockets_state_tx_free(state: *mut c_void, tx_id: u64) { + let state = cast_pointer!(state, WebSocketsState); + state.free_tx(tx_id); +} + +unsafe extern "C" fn rs_websockets_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, WebSocketsState); + state.parse(stream_slice, Direction::ToServer, flow) +} + +unsafe extern "C" fn rs_websockets_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, WebSocketsState); + state.parse(stream_slice, Direction::ToClient, flow) +} + +unsafe extern "C" fn rs_websockets_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void { + let state = cast_pointer!(state, WebSocketsState); + 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_websockets_state_get_tx_count(state: *mut c_void) -> u64 { + let state = cast_pointer!(state, WebSocketsState); + return state.tx_id; +} + +unsafe extern "C" fn rs_websockets_tx_get_alstate_progress( + _tx: *mut c_void, _direction: u8, +) -> c_int { + return 1; +} + +export_tx_data_get!(rs_websockets_get_tx_data, WebSocketsTransaction); +export_state_data_get!(rs_websockets_get_state_data, WebSocketsState); + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"websockets\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_websockets_register_parser() { + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const c_char, + default_port: std::ptr::null(), + ipproto: IPPROTO_TCP, + probe_ts: None, + probe_tc: None, + min_depth: 0, + max_depth: 16, + state_new: rs_websockets_state_new, + state_free: rs_websockets_state_free, + tx_free: rs_websockets_state_tx_free, + parse_ts: rs_websockets_parse_request, + parse_tc: rs_websockets_parse_response, + get_tx_count: rs_websockets_state_get_tx_count, + get_tx: rs_websockets_state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: rs_websockets_tx_get_alstate_progress, + get_eventinfo: None, + get_eventinfo_byid: None, + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some( + applayer::state_get_tx_iterator::, + ), + get_tx_data: rs_websockets_get_tx_data, + get_state_data: rs_websockets_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(WebSocketsFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(WebSocketsFrameType::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_WEBSOCKETS = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + AppLayerRegisterExpectationProto(IPPROTO_TCP, ALPROTO_WEBSOCKETS); + } + SCLogDebug!("Rust websockets parser registered."); + } else { + SCLogDebug!("Protocol detector and parser disabled for WEBSOCKETS."); + } +} diff --git a/src/Makefile.am b/src/Makefile.am index 4695c2d35f51..898e8bcaeaa4 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -429,6 +429,7 @@ noinst_HEADERS = \ output-json-snmp.h \ output-json-ssh.h \ output-json-stats.h \ + output-json-websockets.h \ output-json-template.h \ output-json-tftp.h \ output-json-tls.h \ @@ -1041,6 +1042,7 @@ libsuricata_c_a_SOURCES = \ output-json-snmp.c \ output-json-ssh.c \ output-json-stats.c \ + output-json-websockets.c \ output-json-template.c \ output-json-tftp.c \ output-json-tls.c \ diff --git a/src/app-layer-htp.c b/src/app-layer-htp.c index 5d48611812c1..d704d188d57c 100644 --- a/src/app-layer-htp.c +++ b/src/app-layer-htp.c @@ -53,6 +53,7 @@ #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" @@ -975,11 +976,7 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa 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; @@ -987,17 +984,44 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa 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_WEBSOCKETS) == NULL) { + // if WS is disabled, keep the HTP_STREAM_TUNNEL mode + break; + } + hstate->slice = NULL; + if (AppLayerExpectationCreate(f, STREAM_TOCLIENT | STREAM_TOSERVER, f->sp, + f->dp, ALPROTO_WEBSOCKETS, NULL) < 0) { + HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, + HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE); + } + if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_WEBSOCKETS)) { + 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: diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 1f6066471757..d9c61bce9535 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1764,6 +1764,7 @@ void AppLayerParserRegisterProtocolParsers(void) RegisterSNMPParsers(); RegisterSIPParsers(); RegisterQuicParsers(); + rs_websockets_register_parser(); rs_template_register_parser(); RegisterRFBParsers(); RegisterMQTTParsers(); diff --git a/src/app-layer-protos.c b/src/app-layer-protos.c index 368efacd88d7..6007755aab96 100644 --- a/src/app-layer-protos.c +++ b/src/app-layer-protos.c @@ -60,6 +60,7 @@ const AppProtoStringTuple AppProtoStrings[ALPROTO_MAX] = { { ALPROTO_MQTT, "mqtt" }, { ALPROTO_PGSQL, "pgsql" }, { ALPROTO_TELNET, "telnet" }, + { ALPROTO_WEBSOCKETS, "websockets" }, { ALPROTO_TEMPLATE, "template" }, { ALPROTO_RDP, "rdp" }, { ALPROTO_HTTP2, "http2" }, diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index dd372550cbf5..415fee698c90 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -56,6 +56,7 @@ enum AppProtoEnum { ALPROTO_MQTT, ALPROTO_PGSQL, ALPROTO_TELNET, + ALPROTO_WEBSOCKETS, ALPROTO_TEMPLATE, ALPROTO_RDP, ALPROTO_HTTP2, diff --git a/src/output-json-websockets.c b/src/output-json-websockets.c new file mode 100644 index 000000000000..4e60c1a5d55b --- /dev/null +++ b/src/output-json-websockets.c @@ -0,0 +1,160 @@ +/* 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 + * + * Implement JSON/eve logging app-layer WebSockets. + */ + +#include "suricata-common.h" +#include "detect.h" +#include "pkt-var.h" +#include "conf.h" + +#include "threads.h" +#include "threadvars.h" +#include "tm-threads.h" + +#include "util-unittest.h" +#include "util-buffer.h" +#include "util-debug.h" +#include "util-byte.h" + +#include "output.h" +#include "output-json.h" + +#include "app-layer.h" +#include "app-layer-parser.h" + +#include "output-json-websockets.h" +#include "rust.h" + +typedef struct LogWebSocketsFileCtx_ { + uint32_t flags; + OutputJsonCtx *eve_ctx; +} LogWebSocketsFileCtx; + +typedef struct LogWebSocketsLogThread_ { + LogWebSocketsFileCtx *websocketslog_ctx; + OutputJsonThreadCtx *ctx; +} LogWebSocketsLogThread; + +static int JsonWebSocketsLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f, + void *state, void *tx, uint64_t tx_id) +{ + LogWebSocketsLogThread *thread = thread_data; + + JsonBuilder *js = CreateEveHeader( + p, LOG_DIR_PACKET, "websockets", NULL, thread->websocketslog_ctx->eve_ctx); + if (unlikely(js == NULL)) { + return TM_ECODE_FAILED; + } + + if (!rs_websockets_logger_log(tx, js)) { + goto error; + } + + OutputJsonBuilderBuffer(js, thread->ctx); + jb_free(js); + + return TM_ECODE_OK; + +error: + jb_free(js); + return TM_ECODE_FAILED; +} + +static void OutputWebSocketsLogDeInitCtxSub(OutputCtx *output_ctx) +{ + LogWebSocketsFileCtx *websocketslog_ctx = (LogWebSocketsFileCtx *)output_ctx->data; + SCFree(websocketslog_ctx); + SCFree(output_ctx); +} + +static OutputInitResult OutputWebSocketsLogInitSub(ConfNode *conf, OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + OutputJsonCtx *ajt = parent_ctx->data; + + LogWebSocketsFileCtx *websocketslog_ctx = SCCalloc(1, sizeof(*websocketslog_ctx)); + if (unlikely(websocketslog_ctx == NULL)) { + return result; + } + websocketslog_ctx->eve_ctx = ajt; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx)); + if (unlikely(output_ctx == NULL)) { + SCFree(websocketslog_ctx); + return result; + } + output_ctx->data = websocketslog_ctx; + output_ctx->DeInit = OutputWebSocketsLogDeInitCtxSub; + + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_WEBSOCKETS); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +static TmEcode JsonWebSocketsLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + LogWebSocketsLogThread *thread = SCCalloc(1, sizeof(*thread)); + if (unlikely(thread == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for EveLogWebSockets. \"initdata\" is NULL."); + goto error_exit; + } + + thread->websocketslog_ctx = ((OutputCtx *)initdata)->data; + thread->ctx = CreateEveThreadCtx(t, thread->websocketslog_ctx->eve_ctx); + if (!thread->ctx) { + goto error_exit; + } + *data = (void *)thread; + + return TM_ECODE_OK; + +error_exit: + SCFree(thread); + return TM_ECODE_FAILED; +} + +static TmEcode JsonWebSocketsLogThreadDeinit(ThreadVars *t, void *data) +{ + LogWebSocketsLogThread *thread = (LogWebSocketsLogThread *)data; + if (thread == NULL) { + return TM_ECODE_OK; + } + FreeEveThreadCtx(thread->ctx); + SCFree(thread); + return TM_ECODE_OK; +} + +void JsonWebSocketsLogRegister(void) +{ + /* Register as an eve sub-module. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonWebSocketsLog", "eve-log.websockets", + OutputWebSocketsLogInitSub, ALPROTO_WEBSOCKETS, JsonWebSocketsLogger, + JsonWebSocketsLogThreadInit, JsonWebSocketsLogThreadDeinit, NULL); +} diff --git a/src/output-json-websockets.h b/src/output-json-websockets.h new file mode 100644 index 000000000000..a6ad5fa5b424 --- /dev/null +++ b/src/output-json-websockets.h @@ -0,0 +1,29 @@ +/* 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 FirstName LastName + */ + +#ifndef __OUTPUT_JSON_WEBSOCKETS_H__ +#define __OUTPUT_JSON_WEBSOCKETS_H__ + +void JsonWebSocketsLogRegister(void); + +#endif /* __OUTPUT_JSON_WEBSOCKETS_H__ */ diff --git a/src/output.c b/src/output.c index 149dda58c284..d6b293ca51e6 100644 --- a/src/output.c +++ b/src/output.c @@ -80,6 +80,7 @@ #include "output-json-rfb.h" #include "output-json-mqtt.h" #include "output-json-pgsql.h" +#include "output-json-websockets.h" #include "output-json-template.h" #include "output-json-rdp.h" #include "output-json-http2.h" @@ -1117,6 +1118,8 @@ void OutputRegisterLoggers(void) JsonMQTTLogRegister(); /* Pgsql JSON logger. */ JsonPgsqlLogRegister(); + /* WebSockets JSON logger. */ + JsonWebSocketsLogRegister(); /* Template JSON logger. */ JsonTemplateLogRegister(); /* RDP JSON logger. */ @@ -1159,6 +1162,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = { { ALPROTO_MQTT, JsonMQTTAddMetadata }, { ALPROTO_PGSQL, NULL }, // TODO missing { ALPROTO_TELNET, NULL }, // no logging + { ALPROTO_WEBSOCKETS, rs_websockets_logger_log }, { ALPROTO_TEMPLATE, rs_template_logger_log }, { ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json }, { ALPROTO_HTTP2, rs_http2_log_json }, diff --git a/suricata.yaml.in b/suricata.yaml.in index 630399126dbe..0469b21404d9 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -156,6 +156,7 @@ outputs: header: X-Forwarded-For types: + - websockets - alert: # payload: yes # enable dumping payload in Base64 # payload-buffer-size: 4kb # max size of payload buffer to output in eve-log