mirror of https://github.com/OISF/suricata
bittorrent-dht: add bittorrent-dht app layer
Parses and logs the bittorrent-dht protocol. Note: Includes some compilation fixups after rebase by Jason Ish. Feature: #3086pull/8113/head
parent
27672c950c
commit
86037885a9
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
@ -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;
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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__ */
|
Loading…
Reference in New Issue