]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
bittorrent-dht: add bittorrent-dht app layer
authorAaron Bungay <amb240h@gmail.com>
Fri, 5 Jun 2020 04:17:36 +0000 (00:17 -0400)
committerVictor Julien <vjulien@oisf.net>
Fri, 28 Oct 2022 09:48:12 +0000 (11:48 +0200)
Parses and logs the bittorrent-dht protocol.

Note: Includes some compilation fixups after rebase by Jason Ish.

Feature: #3086

17 files changed:
rust/Cargo.toml.in
rust/src/bittorrent_dht/bittorrent_dht.rs [new file with mode: 0644]
rust/src/bittorrent_dht/logger.rs [new file with mode: 0644]
rust/src/bittorrent_dht/mod.rs [new file with mode: 0644]
rust/src/bittorrent_dht/parser.rs [new file with mode: 0644]
rust/src/lib.rs
src/Makefile.am
src/app-layer-detect-proto.c
src/app-layer-parser.c
src/app-layer-protos.c
src/app-layer-protos.h
src/output-json-alert.c
src/output-json-bittorrent-dht.c [new file with mode: 0644]
src/output-json-bittorrent-dht.h [new file with mode: 0644]
src/output.c
src/util-profiling.c
suricata.yaml.in

index a8fba82aeb9420cf5b3e53457d474ca346260253..67c8bf2339301f4cd6c6d83b20377ae0e29f7163 100644 (file)
@@ -56,6 +56,7 @@ regex = "~1.5.5"
 lazy_static = "~1.4.0"
 base64 = "~0.13.0"
 time = "=0.3.13"
+bendy = { version = "~0.3.3", default-features = false }
 
 suricata-derive = { path = "./derive" }
 
diff --git a/rust/src/bittorrent_dht/bittorrent_dht.rs b/rust/src/bittorrent_dht/bittorrent_dht.rs
new file mode 100644 (file)
index 0000000..615428d
--- /dev/null
@@ -0,0 +1,353 @@
+/* Copyright (C) 2021 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 crate::applayer::{self, *};
+use crate::bittorrent_dht::parser::{
+    parse_bittorrent_dht_packet, BitTorrentDHTError, BitTorrentDHTRequest, BitTorrentDHTResponse,
+};
+use crate::core::{self, AppProto, Flow, ALPROTO_UNKNOWN, IPPROTO_UDP};
+use std::ffi::CString;
+use std::str::FromStr;
+
+const BITTORRENT_DHT_PAYLOAD_PREFIX: &[u8] = b"d1:ad2:id20:";
+const BITTORRENT_DHT_PAYLOAD_PREFIX_LEN: u32 = 12;
+
+static mut ALPROTO_BITTORRENT_DHT: AppProto = ALPROTO_UNKNOWN;
+
+#[repr(u32)]
+#[derive(AppLayerEvent)]
+pub enum BitTorrentDHTEvent {
+    MalformedPacket = 0,
+}
+
+impl BitTorrentDHTEvent {
+    pub fn to_cstring(&self) -> &str {
+        match *self {
+            BitTorrentDHTEvent::MalformedPacket => "malformed_packet\0",
+        }
+    }
+
+    pub fn from_id(id: u32) -> Option<BitTorrentDHTEvent> {
+        match id {
+            0 => Some(BitTorrentDHTEvent::MalformedPacket),
+            _ => None,
+        }
+    }
+}
+
+impl FromStr for BitTorrentDHTEvent {
+    type Err = ();
+
+    fn from_str(s: &str) -> Result<BitTorrentDHTEvent, Self::Err> {
+        match s.to_lowercase().as_ref() {
+            "malformed_packet" => Ok(BitTorrentDHTEvent::MalformedPacket),
+            _ => Err(()),
+        }
+    }
+}
+
+pub struct BitTorrentDHTTransaction {
+    tx_id: u64,
+    pub request_type: Option<String>,
+    pub request: Option<BitTorrentDHTRequest>,
+    pub response: Option<BitTorrentDHTResponse>,
+    pub error: Option<BitTorrentDHTError>,
+    pub transaction_id: String,
+    pub client_version: Option<String>,
+
+    de_state: Option<*mut core::DetectEngineState>,
+    events: *mut core::AppLayerDecoderEvents,
+    tx_data: AppLayerTxData,
+}
+
+impl BitTorrentDHTTransaction {
+    pub fn new() -> BitTorrentDHTTransaction {
+        BitTorrentDHTTransaction {
+            tx_id: 0,
+            request_type: None,
+            request: None,
+            response: None,
+            error: None,
+            transaction_id: String::new(),
+            client_version: None,
+            de_state: None,
+            events: std::ptr::null_mut(),
+            tx_data: AppLayerTxData::new(),
+        }
+    }
+
+    pub fn free(&mut self) {
+        if self.events != std::ptr::null_mut() {
+            core::sc_app_layer_decoder_events_free_events(&mut self.events);
+        }
+        if let Some(state) = self.de_state {
+            core::sc_detect_engine_state_free(state);
+        }
+    }
+
+    /// Set an event on the transaction
+    pub fn set_event(&mut self, event: BitTorrentDHTEvent) {
+        core::sc_app_layer_decoder_events_set_event_raw(&mut self.events, event as u8);
+    }
+}
+
+impl Drop for BitTorrentDHTTransaction {
+    fn drop(&mut self) {
+        self.free();
+    }
+}
+
+#[derive(Default)]
+pub struct BitTorrentDHTState {
+    tx_id: u64,
+    transactions: Vec<BitTorrentDHTTransaction>,
+    state_data: AppLayerStateData,
+}
+
+impl BitTorrentDHTState {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    // Free a transaction by ID.
+    fn free_tx(&mut self, tx_id: u64) {
+        self.transactions.retain(|tx| tx.tx_id != tx_id + 1);
+    }
+
+    pub fn get_tx(&mut self, tx_id: u64) -> Option<&BitTorrentDHTTransaction> {
+        self.transactions.iter().find(|&tx| tx.tx_id == tx_id + 1)
+    }
+
+    fn new_tx(&mut self) -> BitTorrentDHTTransaction {
+        let mut tx = BitTorrentDHTTransaction::new();
+        self.tx_id += 1;
+        tx.tx_id = self.tx_id;
+        return tx;
+    }
+
+    pub fn parse(&mut self, input: &[u8]) -> bool {
+        let mut tx = self.new_tx();
+        let mut status = true;
+
+        if let Err(_e) = parse_bittorrent_dht_packet(input, &mut tx) {
+            status = false;
+            tx.set_event(BitTorrentDHTEvent::MalformedPacket);
+            SCLogDebug!("BitTorrent DHT Parsing Error: {}", _e);
+        }
+
+        self.transactions.push(tx);
+
+        return status;
+    }
+
+    fn tx_iterator(
+        &mut self, min_tx_id: u64, state: &mut u64,
+    ) -> Option<(&BitTorrentDHTTransaction, u64, bool)> {
+        let mut index = *state as usize;
+        let len = self.transactions.len();
+
+        while index < len {
+            let tx = &self.transactions[index];
+            if tx.tx_id < min_tx_id + 1 {
+                index += 1;
+                continue;
+            }
+            *state = index as u64;
+            return Some((tx, tx.tx_id - 1, (len - index) > 1));
+        }
+
+        return None;
+    }
+}
+
+/// Probe to see if this flow looks like BitTorrent DHT
+fn probe(input: &[u8]) -> bool {
+    // Ensure the flow started with a request from the client which
+    // contained the BitTorrent DHT request payload prefix bytes
+    if input.starts_with(BITTORRENT_DHT_PAYLOAD_PREFIX) {
+        return true;
+    }
+    return false;
+}
+
+// C exports.
+
+export_tx_data_get!(rs_bittorrent_dht_get_tx_data, BitTorrentDHTTransaction);
+export_state_data_get!(rs_bittorrent_dht_get_state_data, BitTorrentDHTState);
+
+/// C entry point for BitTorrent DHT probing parser.
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_probing_parser(
+    _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8,
+) -> AppProto {
+    // Need more than BITTORRENT_DHT_PAYLOAD_PREFIX_LEN bytes.
+    if input_len > BITTORRENT_DHT_PAYLOAD_PREFIX_LEN && input != std::ptr::null_mut() {
+        let slice = build_slice!(input, input_len as usize);
+        if probe(slice) {
+            return ALPROTO_BITTORRENT_DHT;
+        }
+    }
+    return ALPROTO_UNKNOWN;
+}
+
+#[no_mangle]
+pub extern "C" fn rs_bittorrent_dht_state_new(
+    _orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto,
+) -> *mut std::os::raw::c_void {
+    let state = BitTorrentDHTState::new();
+    let boxed = Box::new(state);
+    return Box::into_raw(boxed) as *mut std::os::raw::c_void;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_state_free(state: *mut std::os::raw::c_void) {
+    std::mem::drop(Box::from_raw(state as *mut BitTorrentDHTState));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_state_tx_free(
+    state: *mut std::os::raw::c_void, tx_id: u64,
+) {
+    let state = cast_pointer!(state, BitTorrentDHTState);
+    state.free_tx(tx_id);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_parse(
+    _flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void,
+    stream_slice: StreamSlice, _data: *const std::os::raw::c_void,
+) -> AppLayerResult {
+    let state = cast_pointer!(state, BitTorrentDHTState);
+    let buf = stream_slice.as_slice();
+    state.parse(buf).into()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_state_get_tx(
+    state: *mut std::os::raw::c_void, tx_id: u64,
+) -> *mut std::os::raw::c_void {
+    let state = cast_pointer!(state, BitTorrentDHTState);
+    match state.get_tx(tx_id) {
+        Some(tx) => {
+            return tx as *const _ as *mut _;
+        }
+        None => {
+            return std::ptr::null_mut();
+        }
+    }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_state_get_tx_count(
+    state: *mut std::os::raw::c_void,
+) -> u64 {
+    let state = cast_pointer!(state, BitTorrentDHTState);
+    return state.tx_id;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_tx_get_alstate_progress(
+    tx: *mut std::os::raw::c_void, _direction: u8,
+) -> std::os::raw::c_int {
+    let tx = cast_pointer!(tx, BitTorrentDHTTransaction);
+
+    // Transaction is done if we have a request, response, or error since
+    // a new transaction is created for each received packet
+    if tx.request.is_some() || tx.response.is_some() || tx.error.is_some() {
+        return 1;
+    }
+    return 0;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_state_get_events(
+    tx: *mut std::os::raw::c_void,
+) -> *mut core::AppLayerDecoderEvents {
+    let tx = cast_pointer!(tx, BitTorrentDHTTransaction);
+    return tx.events;
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_state_get_tx_iterator(
+    _ipproto: u8, _alproto: AppProto, state: *mut std::os::raw::c_void, min_tx_id: u64,
+    _max_tx_id: u64, istate: &mut u64,
+) -> applayer::AppLayerGetTxIterTuple {
+    let state = cast_pointer!(state, BitTorrentDHTState);
+    match state.tx_iterator(min_tx_id, istate) {
+        Some((tx, out_tx_id, has_next)) => {
+            let c_tx = tx as *const _ as *mut _;
+            let ires = applayer::AppLayerGetTxIterTuple::with_values(c_tx, out_tx_id, has_next);
+            return ires;
+        }
+        None => {
+            return applayer::AppLayerGetTxIterTuple::not_found();
+        }
+    }
+}
+
+// Parser name as a C style string.
+const PARSER_NAME: &[u8] = b"bittorrent-dht\0";
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_udp_register_parser() {
+    let default_port = CString::new("[1024:65535]").unwrap();
+    let parser = RustParser {
+        name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char,
+        default_port: default_port.as_ptr(),
+        ipproto: IPPROTO_UDP,
+        probe_ts: Some(rs_bittorrent_dht_probing_parser),
+        probe_tc: Some(rs_bittorrent_dht_probing_parser),
+        min_depth: 0,
+        max_depth: 16,
+        state_new: rs_bittorrent_dht_state_new,
+        state_free: rs_bittorrent_dht_state_free,
+        tx_free: rs_bittorrent_dht_state_tx_free,
+        parse_ts: rs_bittorrent_dht_parse,
+        parse_tc: rs_bittorrent_dht_parse,
+        get_tx_count: rs_bittorrent_dht_state_get_tx_count,
+        get_tx: rs_bittorrent_dht_state_get_tx,
+        tx_comp_st_ts: 1,
+        tx_comp_st_tc: 1,
+        tx_get_progress: rs_bittorrent_dht_tx_get_alstate_progress,
+        get_eventinfo: Some(BitTorrentDHTEvent::get_event_info),
+        get_eventinfo_byid: Some(BitTorrentDHTEvent::get_event_info_by_id),
+        localstorage_new: None,
+        localstorage_free: None,
+        get_tx_files: None,
+        get_tx_iterator: Some(rs_bittorrent_dht_state_get_tx_iterator),
+        get_tx_data: rs_bittorrent_dht_get_tx_data,
+        get_state_data: rs_bittorrent_dht_get_state_data,
+        apply_tx_config: None,
+        flags: 0,
+        truncate: None,
+        get_frame_id_by_name: None,
+        get_frame_name_by_id: None,
+    };
+
+    let ip_proto_str = CString::new("udp").unwrap();
+
+    if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
+        let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
+        ALPROTO_BITTORRENT_DHT = alproto;
+        if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
+            let _ = AppLayerRegisterParser(&parser, alproto);
+        }
+        SCLogDebug!("Parser registered for bittorrent-dht.");
+    } else {
+        SCLogDebug!("Protocol detector and parser disabled for bittorrent-dht.");
+    }
+}
diff --git a/rust/src/bittorrent_dht/logger.rs b/rust/src/bittorrent_dht/logger.rs
new file mode 100644 (file)
index 0000000..e7246a0
--- /dev/null
@@ -0,0 +1,84 @@
+/* Copyright (C) 2021 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::bittorrent_dht::BitTorrentDHTTransaction;
+use crate::jsonbuilder::{JsonBuilder, JsonError};
+
+fn log_bittorrent_dht(
+    tx: &BitTorrentDHTTransaction, js: &mut JsonBuilder,
+) -> Result<(), JsonError> {
+    js.set_string("transaction_id", &tx.transaction_id)?;
+    if let Some(client_version) = &tx.client_version {
+        js.set_string("client_version", client_version)?;
+    }
+    if let Some(request_type) = &tx.request_type {
+        js.set_string("request_type", request_type)?;
+    }
+    if let Some(error) = &tx.error {
+        js.open_object("error")?;
+        js.set_uint("num", u64::from(error.num))?;
+        js.set_string("msg", &error.msg)?;
+        js.close()?;
+    };
+    if let Some(request) = &tx.request {
+        js.open_object("request")?;
+        js.set_string("id", &request.id)?;
+        if let Some(target) = &request.target {
+            js.set_string("target", target)?;
+        }
+        if let Some(info_hash) = &request.info_hash {
+            js.set_string("info_hash", info_hash)?;
+        }
+        if let Some(token) = &request.token {
+            js.set_string("token", token)?;
+        }
+        if let Some(implied_port) = request.implied_port {
+            js.set_uint("implied_port", u64::from(implied_port))?;
+        }
+        if let Some(port) = request.port {
+            js.set_uint("port", u64::from(port))?;
+        }
+        js.close()?;
+    };
+    if let Some(response) = &tx.response {
+        js.open_object("response")?;
+        js.set_string("id", &response.id)?;
+        if let Some(nodes) = &response.nodes {
+            js.set_string("nodes", nodes)?;
+        }
+        if let Some(values) = &response.values {
+            js.open_array("values")?;
+            for value in values {
+                js.append_string(value)?;
+            }
+            js.close()?;
+        }
+        if let Some(token) = &response.token {
+            js.set_string("token", token)?;
+        }
+        js.close()?;
+    };
+    Ok(())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn rs_bittorrent_dht_logger_log(
+    tx: *mut std::os::raw::c_void, js: &mut JsonBuilder,
+) -> bool {
+    let tx = cast_pointer!(tx, BitTorrentDHTTransaction);
+    log_bittorrent_dht(tx, js).is_ok()
+}
diff --git a/rust/src/bittorrent_dht/mod.rs b/rust/src/bittorrent_dht/mod.rs
new file mode 100644 (file)
index 0000000..0c79ecc
--- /dev/null
@@ -0,0 +1,20 @@
+/* Copyright (C) 2021 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.
+ */
+
+pub mod bittorrent_dht;
+pub mod logger;
+pub mod parser;
diff --git a/rust/src/bittorrent_dht/parser.rs b/rust/src/bittorrent_dht/parser.rs
new file mode 100644 (file)
index 0000000..aae30a8
--- /dev/null
@@ -0,0 +1,602 @@
+/* Copyright (C) 2021 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.
+ */
+
+/*! Parses BitTorrent DHT specification BEP_0005
+ *  <https://www.bittorrent.org/beps/bep_0005.html> !*/
+
+use crate::bittorrent_dht::bittorrent_dht::BitTorrentDHTTransaction;
+use bendy::decoding::{Decoder, Error, FromBencode, Object, ResultExt};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct BitTorrentDHTRequest {
+    /// q = * - 20 byte string, sender's node ID in network byte order
+    pub id: String,
+    /// q = find_node - target node ID
+    pub target: Option<String>,
+    /// q = get_peers/announce_peer - 20-byte info hash of target torrent
+    pub info_hash: Option<String>,
+    /// q = announce_peer - token key received from previous get_peers query
+    pub token: Option<String>,
+    /// q = announce_peer - 0 or 1, if 1 ignore provided port and
+    ///                     use source port of UDP packet
+    pub implied_port: Option<u8>,
+    /// q = announce_peer - port on which peer will download torrent
+    pub port: Option<u16>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct BitTorrentDHTResponse {
+    /// q = * - 20 byte string, receiver's node ID in network byte order
+    pub id: String,
+    /// q = find_node/get_peers - compact node info for target node or
+    ///                           K(8) closest good nodes in routing table
+    pub nodes: Option<String>,
+    /// q = get_peers - list of compact peer infos
+    pub values: Option<Vec<String>>,
+    /// q = get_peers - token key required for sender's future
+    ///                 announce_peer query
+    pub token: Option<String>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct BitTorrentDHTError {
+    /// integer representing the error code
+    pub num: u16,
+    /// string containing the error message
+    pub msg: String,
+}
+
+impl FromBencode for BitTorrentDHTRequest {
+    // Try to parse with a `max_depth` of one.
+    //
+    // The required max depth of a data structure is calculated as follows:
+    //  - every potential nesting level encoded as bencode dictionary or
+    //    list count as +1,
+    //  - everything else is ignored.
+    //
+    // struct BitTorrentDHTRequest {   // encoded as dictionary (+1)
+    //     id: String,
+    //     target: Option<String>,
+    //     info_hash: Option<String>,
+    //     token: Option<String>,
+    //     implied_port: Option<u8>,
+    //     port: Option<u16>,
+    // }
+    const EXPECTED_RECURSION_DEPTH: usize = 1;
+
+    fn decode_bencode_object(object: Object) -> Result<Self, Error>
+    where
+        Self: Sized,
+    {
+        let mut id = None;
+        let mut target = None;
+        let mut info_hash = None;
+        let mut token = None;
+        let mut implied_port = None;
+        let mut port = None;
+
+        let mut dict_dec = object.try_into_dictionary()?;
+
+        while let Some(pair) = dict_dec.next_pair()? {
+            match pair {
+                (b"id", value) => {
+                    id = String::decode_bencode_object(value)
+                        .context("id")
+                        .map(Some)?;
+                }
+                (b"target", value) => {
+                    target = String::decode_bencode_object(value)
+                        .context("target")
+                        .map(Some)?;
+                }
+                (b"info_hash", value) => {
+                    info_hash = String::decode_bencode_object(value)
+                        .context("info_hash")
+                        .map(Some)?;
+                }
+                (b"token", value) => {
+                    token = String::decode_bencode_object(value)
+                        .context("token")
+                        .map(Some)?;
+                }
+                (b"implied_port", value) => {
+                    implied_port = u8::decode_bencode_object(value)
+                        .context("implied_port")
+                        .map(Some)?
+                }
+                (b"port", value) => {
+                    port = u16::decode_bencode_object(value)
+                        .context("port")
+                        .map(Some)?
+                }
+                (_unknown_field, _) => {}
+            }
+        }
+
+        let id = id.ok_or_else(|| Error::missing_field("id"))?;
+
+        Ok(BitTorrentDHTRequest {
+            id,
+            target,
+            info_hash,
+            token,
+            implied_port,
+            port,
+        })
+    }
+}
+
+impl FromBencode for BitTorrentDHTResponse {
+    // Try to parse with a `max_depth` of two.
+    //
+    // The required max depth of a data structure is calculated as follows:
+    //  - every potential nesting level encoded as bencode dictionary or
+    //    list count as +1,
+    //  - everything else is ignored.
+    //
+    // struct BitTorrentDHTResponse {   // encoded as dictionary (+1)
+    //     id: String,
+    //     nodes: Option<String>,
+    //     values: Option<Vec<String>>, // if present, encoded as list (+1)
+    //     token: Option<String>,
+    // }
+    const EXPECTED_RECURSION_DEPTH: usize = 2;
+
+    fn decode_bencode_object(object: Object) -> Result<Self, Error>
+    where
+        Self: Sized,
+    {
+        let mut id = None;
+        let mut nodes = None;
+        let mut values = None;
+        let mut token = None;
+
+        let mut dict_dec = object.try_into_dictionary()?;
+
+        while let Some(pair) = dict_dec.next_pair()? {
+            match pair {
+                (b"id", value) => {
+                    id = String::decode_bencode_object(value)
+                        .context("id")
+                        .map(Some)?;
+                }
+                (b"nodes", value) => {
+                    nodes = String::decode_bencode_object(value)
+                        .context("nodes")
+                        .map(Some)?;
+                }
+                (b"values", value) => {
+                    values = Vec::decode_bencode_object(value)
+                        .context("values")
+                        .map(Some)?;
+                }
+                (b"token", value) => {
+                    token = String::decode_bencode_object(value)
+                        .context("token")
+                        .map(Some)?;
+                }
+                (_unknown_field, _) => {}
+            }
+        }
+
+        let id = id.ok_or_else(|| Error::missing_field("id"))?;
+
+        Ok(BitTorrentDHTResponse {
+            id,
+            nodes,
+            values,
+            token,
+        })
+    }
+}
+
+impl FromBencode for BitTorrentDHTError {
+    // Try to parse with a `max_depth` of one.
+    //
+    // The required max depth of a data structure is calculated as follows:
+    //  - every potential nesting level encoded as bencode dictionary or
+    //    list count as +1,
+    //  - everything else is ignored.
+    //
+    // struct BitTorrentDHTError {   // encoded as dictionary (+1)
+    //     num: u16,
+    //     msg: String,
+    // }
+    const EXPECTED_RECURSION_DEPTH: usize = 1;
+
+    fn decode_bencode_object(object: Object) -> Result<Self, Error>
+    where
+        Self: Sized,
+    {
+        let mut num = None;
+        let mut msg = None;
+
+        let mut list_dec = object.try_into_list()?;
+
+        while let Some(object) = list_dec.next_object()? {
+            match object {
+                Object::Integer(_) => {
+                    num = u16::decode_bencode_object(object)
+                        .context("num")
+                        .map(Some)?;
+                }
+                Object::Bytes(_) => {
+                    msg = String::decode_bencode_object(object)
+                        .context("msg")
+                        .map(Some)?;
+                }
+                _ => {}
+            }
+        }
+
+        let num = num.ok_or_else(|| Error::missing_field("num"))?;
+        let msg = msg.ok_or_else(|| Error::missing_field("msg"))?;
+
+        Ok(BitTorrentDHTError { num, msg })
+    }
+}
+
+pub fn parse_bittorrent_dht_packet(
+    bytes: &[u8], tx: &mut BitTorrentDHTTransaction,
+) -> Result<(), Error> {
+    // Try to parse with a `max_depth` of three.
+    //
+    // The required max depth of a data structure is calculated as follows:
+    //  - every potential nesting level encoded as bencode dictionary or
+    //    list count as +1,
+    //  - everything else is ignored.
+    //
+    // - Outer packet is a dictionary (+1)
+    // - Max depth of child within dictionary is a BitTorrentDHTResponse (+2)
+    let mut decoder = Decoder::new(bytes).with_max_depth(3);
+    let object = decoder.next_object()?;
+
+    let mut packet_type = None;
+    let mut query_type = None;
+    let mut query_arguments = None;
+    let mut response = None;
+    let mut error = None;
+    let mut transaction_id = None;
+    let mut client_version = None;
+
+    let mut dict_dec = object
+        .ok_or_else(|| Error::unexpected_token("Dict", "EOF"))?
+        .try_into_dictionary()?;
+
+    while let Some(pair) = dict_dec.next_pair()? {
+        match pair {
+            (b"y", value) => {
+                // q (query) vs r (response) vs e (error)
+                packet_type = String::decode_bencode_object(value)
+                    .context("packet_type")
+                    .map(Some)?;
+            }
+            (b"q", value) => {
+                // query type found
+                query_type = String::decode_bencode_object(value)
+                    .context("query_type")
+                    .map(Some)?;
+            }
+            (b"a", value) => {
+                // query arguments found
+                query_arguments = BitTorrentDHTRequest::decode_bencode_object(value)
+                    .context("query_arguments")
+                    .map(Some)?;
+            }
+            (b"r", value) => {
+                // response found
+                response = BitTorrentDHTResponse::decode_bencode_object(value)
+                    .context("response")
+                    .map(Some)?;
+            }
+            (b"e", value) => {
+                // error found
+                error = BitTorrentDHTError::decode_bencode_object(value)
+                    .context("error")
+                    .map(Some)?;
+            }
+            (b"t", value) => {
+                // transaction id found
+                transaction_id = String::decode_bencode_object(value)
+                    .context("transaction_id")
+                    .map(Some)?;
+            }
+            (b"v", value) => {
+                // client version string found
+                client_version = String::decode_bencode_object(value)
+                    .context("client_version")
+                    .map(Some)?;
+            }
+            (_unknown_field, _) => {}
+        }
+    }
+
+    if let Some(t) = packet_type {
+        match t.as_str() {
+            "q" => {
+                tx.request_type =
+                    Some(query_type.ok_or_else(|| Error::missing_field("query_type"))?);
+                tx.request =
+                    Some(query_arguments.ok_or_else(|| Error::missing_field("query_arguments"))?);
+            }
+            "r" => {
+                tx.response = Some(response.ok_or_else(|| Error::missing_field("response"))?);
+            }
+            "e" => {
+                tx.error = Some(error.ok_or_else(|| Error::missing_field("error"))?);
+            }
+            v => {
+                return Err(Error::unexpected_token("packet_type q, r, or e", v));
+            }
+        }
+    } else {
+        return Err(Error::missing_field("packet_type"));
+    }
+
+    tx.transaction_id = transaction_id.ok_or_else(|| Error::missing_field("transaction_id"))?;
+    // Client version string is an optional field
+    tx.client_version = client_version;
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use test_case::test_case;
+
+    #[test_case(
+        b"d2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe",
+        BitTorrentDHTRequest { id: "abcdefghij0123456789".to_string(), implied_port: Some(1u8), info_hash: Some("mnopqrstuvwxyz123456".to_string()), port: Some(6881u16), token: Some("aoeusnth".to_string()), target: None } ;
+        "test request from bencode 1")]
+    #[test_case(
+        b"d2:id20:abcdefghij0123456789e",
+        BitTorrentDHTRequest { id: "abcdefghij0123456789".to_string(), implied_port: None, info_hash: None, port: None, token: None, target: None } ;
+        "test request from bencode 2")]
+    #[test_case(
+        b"d2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e",
+        BitTorrentDHTRequest { id: "abcdefghij0123456789".to_string(), implied_port: None, info_hash: None, port: None, token: None, target: Some("mnopqrstuvwxyz123456".to_string()) } ;
+        "test request from bencode 3")]
+    #[test_case(
+        b"d2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e",
+        BitTorrentDHTRequest { id: "abcdefghij0123456789".to_string(), implied_port: None, info_hash: Some("mnopqrstuvwxyz123456".to_string()), port: None, token: None, target: None } ;
+        "test request from bencode 4")]
+    fn test_request_from_bencode(encoded: &[u8], expected: BitTorrentDHTRequest) {
+        let decoded = BitTorrentDHTRequest::from_bencode(encoded).unwrap();
+        assert_eq!(expected, decoded);
+    }
+
+    #[test_case(
+        b"d12:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe",
+        "Error: missing field: id" ;
+        "test request from bencode err 1")]
+    #[test_case(
+        b"d2:id20:abcdefghij012345678912:implied_porti9999e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe",
+        "Error: malformed content discovered in implied_port" ;
+        "test request from bencode err 2")]
+    #[test_case(
+        b"d2:id20:abcdefghij012345678912:implied_porti-1e9:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe",
+        "Error: malformed content discovered in implied_port" ;
+        "test request from bencode err 3")]
+    #[test_case(
+        b"d2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti9999999e5:token8:aoeusnthe",
+        "Error: malformed content discovered in port" ;
+        "test request from bencode err 4")]
+    #[test_case(
+        b"d2:id20:abcdefghij012345678912:implied_porti1e9:info_hash20:mnopqrstuvwxyz1234564:porti-1e5:token8:aoeusnthe",
+        "Error: malformed content discovered in port" ;
+        "test request from bencode err 5")]
+    #[test_case(
+        b"i123e",
+        "Error: discovered Dict but expected Num" ;
+        "test request from bencode err 6")]
+    fn test_request_from_bencode_err(encoded: &[u8], expected_error: &str) {
+        let err = BitTorrentDHTRequest::from_bencode(encoded).unwrap_err();
+        assert_eq!(expected_error, err.to_string());
+    }
+
+    #[test_case(
+        b"d2:id20:abcdefghij01234567895:token8:aoeusnth6:valueslee",
+        BitTorrentDHTResponse { id: "abcdefghij0123456789".to_string(), token: Some("aoeusnth".to_string()), values: Some(vec![]), nodes: None } ;
+        "test response from bencode 1")]
+    #[test_case(
+        b"d2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.uee",
+        BitTorrentDHTResponse { id: "abcdefghij0123456789".to_string(), token: Some("aoeusnth".to_string()), values: Some(vec!["axje.u".to_string()]), nodes: None } ;
+        "test response from bencode 2")]
+    #[test_case(
+        b"d2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee",
+        BitTorrentDHTResponse { id: "abcdefghij0123456789".to_string(), token: Some("aoeusnth".to_string()), values: Some(vec!["axje.u".to_string(), "idhtnm".to_string()]), nodes: None } ;
+        "test response from bencode 3")]
+    #[test_case(
+        b"d2:id20:mnopqrstuvwxyz123456e",
+        BitTorrentDHTResponse { id: "mnopqrstuvwxyz123456".to_string(), token: None, values: None, nodes: None } ;
+        "test response from bencode 4")]
+    #[test_case(
+        b"d2:id20:0123456789abcdefghij5:nodes9:def456...e",
+        BitTorrentDHTResponse { id: "0123456789abcdefghij".to_string(), token: None, values: None, nodes: Some("def456...".to_string()) } ;
+        "test response from bencode 5")]
+    #[test_case(
+        b"d2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe",
+        BitTorrentDHTResponse { id: "abcdefghij0123456789".to_string(), token: Some("aoeusnth".to_string()), values: None, nodes: Some("def456...".to_string()) } ;
+        "test response from bencode 6")]
+    fn test_response_from_bencode(encoded: &[u8], expected: BitTorrentDHTResponse) {
+        let decoded = BitTorrentDHTResponse::from_bencode(encoded).unwrap();
+        assert_eq!(expected, decoded);
+    }
+
+    #[test_case(
+        b"d5:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee",
+        "Error: missing field: id" ;
+        "test response from bencode err 1")]
+    #[test_case(
+        b"i123e",
+        "Error: discovered Dict but expected Num" ;
+        "test response from bencode err 2")]
+    fn test_response_from_bencode_err(encoded: &[u8], expected_error: &str) {
+        let err = BitTorrentDHTResponse::from_bencode(encoded).unwrap_err();
+        assert_eq!(expected_error, err.to_string());
+    }
+
+    #[test_case(
+        b"li201e23:A Generic Error Ocurrede",
+        BitTorrentDHTError { num: 201u16, msg: "A Generic Error Ocurred".to_string() } ;
+        "test error from bencode 1")]
+    #[test_case(
+        b"li202e12:Server Errore",
+        BitTorrentDHTError { num: 202u16, msg: "Server Error".to_string() } ;
+        "test error from bencode 2")]
+    #[test_case(
+        b"li203e14:Protocol Errore",
+        BitTorrentDHTError { num: 203u16, msg: "Protocol Error".to_string() } ;
+        "test error from bencode 3")]
+    #[test_case(
+        b"li204e14:Method Unknowne",
+        BitTorrentDHTError { num: 204u16, msg: "Method Unknown".to_string() } ;
+        "test error from bencode 4")]
+    fn test_error_from_bencode(encoded: &[u8], expected: BitTorrentDHTError) {
+        let decoded = BitTorrentDHTError::from_bencode(encoded).unwrap();
+        assert_eq!(expected, decoded);
+    }
+
+    #[test_case(
+        b"l23:A Generic Error Ocurrede",
+        "Error: missing field: num" ;
+        "test error from bencode err 1")]
+    #[test_case(
+        b"li201ee",
+        "Error: missing field: msg" ;
+        "test error from bencode err 2")]
+    #[test_case(
+        b"li999999ee",
+        "Error: malformed content discovered in num" ;
+        "test error from bencode err 3")]
+    #[test_case(
+        b"li-1ee",
+        "Error: malformed content discovered in num" ;
+        "test error from bencode err 4")]
+    #[test_case(
+        b"i123e",
+        "Error: discovered List but expected Num" ;
+        "test error from bencode err 5")]
+    fn test_error_from_bencode_err(encoded: &[u8], expected_error: &str) {
+        let err = BitTorrentDHTError::from_bencode(encoded).unwrap_err();
+        assert_eq!(expected_error, err.to_string());
+    }
+
+    #[test_case(
+        b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:v4:UT011:y1:qe",
+        Some("ping".to_string()),
+        Some(BitTorrentDHTRequest { id: "abcdefghij0123456789".to_string(), implied_port: None, info_hash: None, port: None, token: None, target: None }),
+        None,
+        None,
+        "aa".to_string(),
+        Some("UT01".to_string()) ;
+        "test parse bittorrent dht packet 1"
+    )]
+    #[test_case(
+        b"d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:re",
+        None,
+        None,
+        Some(BitTorrentDHTResponse { id: "abcdefghij0123456789".to_string(), token: Some("aoeusnth".to_string()), values: Some(vec!["axje.u".to_string(), "idhtnm".to_string()]), nodes: None}),
+        None,
+        "aa".to_string(),
+        None ;
+        "test parse bittorrent dht packet 2"
+    )]
+    #[test_case(
+        b"d1:eli201e23:A Generic Error Ocurrede1:t2:aa1:v4:UT011:y1:ee",
+        None,
+        None,
+        None,
+        Some(BitTorrentDHTError { num: 201u16, msg: "A Generic Error Ocurred".to_string() }),
+        "aa".to_string(),
+        Some("UT01".to_string()) ;
+        "test parse bittorrent dht packet 3"
+    )]
+    fn test_parse_bittorrent_dht_packet(
+        encoded: &[u8], request_type: Option<String>,
+        expected_request: Option<BitTorrentDHTRequest>,
+        expected_response: Option<BitTorrentDHTResponse>,
+        expected_error: Option<BitTorrentDHTError>, expected_transaction_id: String,
+        expected_client_version: Option<String>,
+    ) {
+        let mut tx = BitTorrentDHTTransaction::new();
+        parse_bittorrent_dht_packet(encoded, &mut tx).unwrap();
+        assert_eq!(request_type, tx.request_type);
+        assert_eq!(expected_request, tx.request);
+        assert_eq!(expected_response, tx.response);
+        assert_eq!(expected_error, tx.error);
+        assert_eq!(expected_transaction_id, tx.transaction_id);
+        assert_eq!(expected_client_version, tx.client_version);
+    }
+
+    #[test_case(
+        b"",
+        "Error: discovered Dict but expected EOF" ;
+        "test parse bittorrent dht packet err 1"
+    )]
+    #[test_case(
+        b"li2123ei321ee",
+        "Error: discovered Dict but expected List" ;
+        "test parse bittorrent dht packet err 2"
+    )]
+    #[test_case(
+        b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aae",
+        "Error: missing field: packet_type" ;
+        "test parse bittorrent dht packet err 3"
+    )]
+    #[test_case(
+        b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:Fe",
+        "Error: discovered packet_type q, r, or e but expected F" ;
+        "test parse bittorrent dht packet err 4"
+    )]
+    #[test_case(
+        b"d1:ad2:id20:abcdefghij0123456789e1:t2:aa1:y1:qe",
+        "Error: missing field: query_type" ;
+        "test parse bittorrent dht packet err 5"
+    )]
+    #[test_case(
+        b"d1:q4:ping1:t2:aa1:y1:qe",
+        "Error: missing field: query_arguments" ;
+        "test parse bittorrent dht packet err 6"
+    )]
+    #[test_case(
+        b"d1:t2:aa1:y1:re",
+        "Error: missing field: response" ;
+        "test parse bittorrent dht packet err 7"
+    )]
+    #[test_case(
+        b"d1:t2:aa1:y1:ee",
+        "Error: missing field: error" ;
+        "test parse bittorrent dht packet err 8"
+    )]
+    #[test_case(
+        b"d1:ade1:q4:ping1:t2:aa1:y1:qe",
+        "Error: missing field: id in query_arguments" ;
+        "test parse bittorrent dht packet err 9"
+    )]
+    #[test_case(
+        b"d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:y1:qe",
+        "Error: missing field: transaction_id" ;
+        "test parse bittorrent dht packet err 10"
+    )]
+    fn test_parse_bittorrent_dht_packet_err(encoded: &[u8], expected_error: &str) {
+        let mut tx = BitTorrentDHTTransaction::new();
+        let err = parse_bittorrent_dht_packet(encoded, &mut tx).unwrap_err();
+        assert_eq!(expected_error, err.to_string());
+    }
+}
index e5c3cacd762e8949eb80ca563192e4014219c353..175053fc088c3a54eee4a4c585fd95f72f72d99e 100644 (file)
@@ -118,6 +118,7 @@ pub mod mime;
 pub mod ssh;
 pub mod http2;
 pub mod quic;
+pub mod bittorrent_dht;
 pub mod plugin;
 pub mod util;
 pub mod ffi;
index 3ba7e5e1197d84ee17692c023d4c047957b57260..a019992345800394ea4726f7e19d7af4e24e9468 100755 (executable)
@@ -396,6 +396,7 @@ noinst_HEADERS = \
        output.h \
        output-json-alert.h \
        output-json-anomaly.h \
+       output-json-bittorrent-dht.h \
        output-json-dcerpc.h \
        output-json-dhcp.h \
        output-json-dnp3.h \
@@ -1003,6 +1004,7 @@ libsuricata_c_a_SOURCES = \
        output-flow.c \
        output-json-alert.c \
        output-json-anomaly.c \
+       output-json-bittorrent-dht.c \
        output-json.c \
        output-json-common.c \
        output-json-dcerpc.c \
index ceb0474a81bd668fe72c5636b841da0431fad379..d66f7e27cb29bda706cf0d24585838dfed58ab90 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (C) 2007-2014 Open Information Security Foundation
+/* Copyright (C) 2007-2021 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
@@ -920,6 +920,8 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar
                         printf("            alproto: ALPROTO_TEMPLATE\n");
                     else if (pp_pe->alproto == ALPROTO_DNP3)
                         printf("            alproto: ALPROTO_DNP3\n");
+                    else if (pp_pe->alproto == ALPROTO_BITTORRENT_DHT)
+                        printf("            alproto: ALPROTO_BITTORRENT_DHT\n");
                     else
                         printf("impossible\n");
 
@@ -1003,6 +1005,8 @@ static void AppLayerProtoDetectPrintProbingParsers(AppLayerProtoDetectProbingPar
                     printf("            alproto: ALPROTO_TEMPLATE\n");
                 else if (pp_pe->alproto == ALPROTO_DNP3)
                     printf("            alproto: ALPROTO_DNP3\n");
+                else if (pp_pe->alproto == ALPROTO_BITTORRENT_DHT)
+                    printf("            alproto: ALPROTO_BITTORRENT_DHT\n");
                 else
                     printf("impossible\n");
 
index 31c4c8ade5134292351042ad5db02d6dd9e34044..c65a1c2af291412faea3f83568ab6cb6fd6eb71a 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (C) 2007-2020 Open Information Security Foundation
+/* Copyright (C) 2007-2021 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
@@ -1723,6 +1723,7 @@ void AppLayerParserRegisterProtocolParsers(void)
     RegisterSMTPParsers();
     rs_dns_udp_register_parser();
     rs_dns_tcp_register_parser();
+    rs_bittorrent_dht_udp_register_parser();
     RegisterModbusParsers();
     RegisterENIPUDPParsers();
     RegisterENIPTCPParsers();
index 823e758621db40ae57572b1e2306e6951e4d1866..d18d2f699098fc15e14495e1bf08cafdf7c41d6d 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (C) 2007-2013 Open Information Security Foundation
+/* Copyright (C) 2007-2021 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
@@ -132,6 +132,9 @@ const char *AppProtoToString(AppProto alproto)
         case ALPROTO_HTTP:
             proto_name = "http_any";
             break;
+        case ALPROTO_BITTORRENT_DHT:
+            proto_name = "bittorrent-dht";
+            break;
         case ALPROTO_FAILED:
             proto_name = "failed";
             break;
@@ -191,6 +194,8 @@ AppProto StringToAppProto(const char *proto_name)
     if (strcmp(proto_name,"template-rust")==0) return ALPROTO_TEMPLATE_RUST;
     if (strcmp(proto_name,"rdp")==0) return ALPROTO_RDP;
     if (strcmp(proto_name,"http2")==0) return ALPROTO_HTTP2;
+    if (strcmp(proto_name, "bittorrent-dht") == 0)
+        return ALPROTO_BITTORRENT_DHT;
     if (strcmp(proto_name,"failed")==0) return ALPROTO_FAILED;
 
     return ALPROTO_UNKNOWN;
index 3bc023c5999498f958a00aff918c0a84b9489615..0c29135db6c23776b4e707809206ef189a7c97f3 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (C) 2007-2013 Open Information Security Foundation
+/* Copyright (C) 2007-2021 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
@@ -60,6 +60,7 @@ enum AppProtoEnum {
     ALPROTO_TEMPLATE_RUST,
     ALPROTO_RDP,
     ALPROTO_HTTP2,
+    ALPROTO_BITTORRENT_DHT,
 
     // signature-only (ie not seen in flow)
     // HTTP for any version (ALPROTO_HTTP1 (version 1) or ALPROTO_HTTP2)
index 3ec980b83c87f9135601b33d00b2a57e2dc56fe2..5b72dfba3c61a0342ec14b73aedb555ade5d2677 100644 (file)
@@ -274,6 +274,25 @@ static void AlertJsonRDP(const Flow *f, const uint64_t tx_id, JsonBuilder *js)
     }
 }
 
+static void AlertJsonBitTorrentDHT(const Flow *f, const uint64_t tx_id, JsonBuilder *js)
+{
+    void *bittorrent_dht_state = (void *)FlowGetAppState(f);
+    if (bittorrent_dht_state != NULL) {
+        void *tx =
+                AppLayerParserGetTx(f->proto, ALPROTO_BITTORRENT_DHT, bittorrent_dht_state, tx_id);
+        if (tx != NULL) {
+            JsonBuilderMark mark = { 0, 0, 0 };
+            jb_get_mark(js, &mark);
+            jb_open_object(js, "bittorrent-dht");
+            if (rs_bittorrent_dht_logger_log(tx, js)) {
+                jb_close(js);
+            } else {
+                jb_restore_mark(js, &mark);
+            }
+        }
+    }
+}
+
 static void AlertJsonSourceTarget(const Packet *p, const PacketAlert *pa,
                                   JsonBuilder *js, JsonAddrInfo *addr)
 {
@@ -568,6 +587,9 @@ static void AlertAddAppLayer(const Packet *p, JsonBuilder *jb,
                 jb_restore_mark(jb, &mark);
             }
             break;
+        case ALPROTO_BITTORRENT_DHT:
+            AlertJsonBitTorrentDHT(p->flow, tx_id, jb);
+            break;
         default:
             break;
     }
diff --git a/src/output-json-bittorrent-dht.c b/src/output-json-bittorrent-dht.c
new file mode 100644 (file)
index 0000000..64d7ff8
--- /dev/null
@@ -0,0 +1,165 @@
+/* Copyright (C) 2021 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
+ *
+ * Implement JSON/eve logging app-layer BitTorrent DHT.
+ */
+
+#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-bittorrent-dht.h"
+#include "rust.h"
+
+typedef struct LogBitTorrentDHTFileCtx_ {
+    uint32_t flags;
+    OutputJsonCtx *eve_ctx;
+} LogBitTorrentDHTFileCtx;
+
+typedef struct LogBitTorrentDHTLogThread_ {
+    LogBitTorrentDHTFileCtx *bittorrent_dht_log_ctx;
+    OutputJsonThreadCtx *ctx;
+} LogBitTorrentDHTLogThread;
+
+static int JsonBitTorrentDHTLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f,
+        void *state, void *tx, uint64_t tx_id)
+{
+    LogBitTorrentDHTLogThread *thread = thread_data;
+
+    JsonBuilder *js = CreateEveHeader(
+            p, LOG_DIR_PACKET, "bittorrent-dht", NULL, thread->bittorrent_dht_log_ctx->eve_ctx);
+    if (unlikely(js == NULL)) {
+        return TM_ECODE_FAILED;
+    }
+
+    jb_open_object(js, "bittorrent-dht");
+    if (!rs_bittorrent_dht_logger_log(tx, js)) {
+        goto error;
+    }
+    jb_close(js);
+
+    OutputJsonBuilderBuffer(js, thread->ctx);
+    jb_free(js);
+
+    return TM_ECODE_OK;
+
+error:
+    jb_free(js);
+    return TM_ECODE_FAILED;
+}
+
+static void OutputBitTorrentDHTLogDeInitCtxSub(OutputCtx *output_ctx)
+{
+    LogBitTorrentDHTFileCtx *bittorrent_dht_log_ctx = (LogBitTorrentDHTFileCtx *)output_ctx->data;
+    SCFree(bittorrent_dht_log_ctx);
+    SCFree(output_ctx);
+}
+
+static OutputInitResult OutputBitTorrentDHTLogInitSub(ConfNode *conf, OutputCtx *parent_ctx)
+{
+    OutputInitResult result = { NULL, false };
+    OutputJsonCtx *ajt = parent_ctx->data;
+
+    LogBitTorrentDHTFileCtx *bittorrent_dht_log_ctx = SCCalloc(1, sizeof(*bittorrent_dht_log_ctx));
+    if (unlikely(bittorrent_dht_log_ctx == NULL)) {
+        return result;
+    }
+    bittorrent_dht_log_ctx->eve_ctx = ajt;
+
+    OutputCtx *output_ctx = SCCalloc(1, sizeof(*output_ctx));
+    if (unlikely(output_ctx == NULL)) {
+        SCFree(bittorrent_dht_log_ctx);
+        return result;
+    }
+    output_ctx->data = bittorrent_dht_log_ctx;
+    output_ctx->DeInit = OutputBitTorrentDHTLogDeInitCtxSub;
+
+    AppLayerParserRegisterLogger(IPPROTO_UDP, ALPROTO_BITTORRENT_DHT);
+
+    result.ctx = output_ctx;
+    result.ok = true;
+    return result;
+}
+
+static TmEcode JsonBitTorrentDHTLogThreadInit(ThreadVars *t, const void *initdata, void **data)
+{
+    LogBitTorrentDHTLogThread *thread = SCCalloc(1, sizeof(*thread));
+    if (unlikely(thread == NULL)) {
+        return TM_ECODE_FAILED;
+    }
+
+    if (initdata == NULL) {
+        SCLogDebug("Error getting context for EveLogBitTorrentDHT.  \"initdata\" is NULL.");
+        goto error_exit;
+    }
+
+    thread->bittorrent_dht_log_ctx = ((OutputCtx *)initdata)->data;
+    thread->ctx = CreateEveThreadCtx(t, thread->bittorrent_dht_log_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 JsonBitTorrentDHTLogThreadDeinit(ThreadVars *t, void *data)
+{
+    LogBitTorrentDHTLogThread *thread = (LogBitTorrentDHTLogThread *)data;
+    if (thread == NULL) {
+        return TM_ECODE_OK;
+    }
+    FreeEveThreadCtx(thread->ctx);
+    SCFree(thread);
+    return TM_ECODE_OK;
+}
+
+void JsonBitTorrentDHTLogRegister(void)
+{
+    if (ConfGetNode("app-layer.protocols.bittorrent-dht") == NULL) {
+        return;
+    }
+
+    /* Register as an eve sub-module. */
+    OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonBitTorrentDHTLog",
+            "eve-log.bittorrent-dht", OutputBitTorrentDHTLogInitSub, ALPROTO_BITTORRENT_DHT,
+            JsonBitTorrentDHTLogger, JsonBitTorrentDHTLogThreadInit,
+            JsonBitTorrentDHTLogThreadDeinit, NULL);
+}
diff --git a/src/output-json-bittorrent-dht.h b/src/output-json-bittorrent-dht.h
new file mode 100644 (file)
index 0000000..8927f4d
--- /dev/null
@@ -0,0 +1,27 @@
+/* Copyright (C) 2021 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
+ */
+
+#ifndef __OUTPUT_JSON_BITTORRENT_DHT_H__
+#define __OUTPUT_JSON_BITTORRENT_DHT_H__
+
+void JsonBitTorrentDHTLogRegister(void);
+
+#endif /* __OUTPUT_JSON_BITTORRENT_DHT_H__ */
index bcf4e26255f29c02fb75c98b7b29ebed08e10cd1..a9929f048d510e8c4c670383338f3febd9efa668 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (C) 2007-2016 Open Information Security Foundation
+/* Copyright (C) 2007-2021 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
@@ -87,6 +87,7 @@
 #include "output-json-metadata.h"
 #include "output-json-dcerpc.h"
 #include "output-json-frame.h"
+#include "output-json-bittorrent-dht.h"
 #include "output-filestore.h"
 
 typedef struct RootLogger_ {
@@ -1126,4 +1127,6 @@ void OutputRegisterLoggers(void)
     JsonDCERPCLogRegister();
     /* app layer frames */
     JsonFrameLogRegister();
+    /* BitTorrent DHT JSON logger */
+    JsonBitTorrentDHTLogRegister();
 }
index 55b5029fd29d0e6bcdb0e9a458d033e65ce63e37..aadbe66ac5501ca0a0e20d2c9d51a4016b055174 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (C) 2007-2012 Open Information Security Foundation
+/* Copyright (C) 2007-2021 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
index e1a2ca932d056580c8527adba13bf6e0d22ce8f4..5988dfbf679c1bbd8a3ae42a5c87eb97bdc22b65 100644 (file)
@@ -280,6 +280,7 @@ outputs:
         - ike
         - dcerpc
         - krb5
+        - bittorrent-dht
         - snmp
         - rfb
         - sip
@@ -808,6 +809,8 @@ app-layer:
       # max-tx: 4096
     krb5:
       enabled: yes
+    bittorrent-dht:
+      enabled: yes
     snmp:
       enabled: yes
     ike: