Skip to content

Commit db7206c

Browse files
committed
(feat/webrtc-sniffer): implement dtls parser
1 parent 354aa73 commit db7206c

File tree

5 files changed

+224
-44
lines changed

5 files changed

+224
-44
lines changed

Cargo.lock

Lines changed: 19 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/webrtc-sniffer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ sudo = { version = "0.6.0" }
1313
hex = { version = "0.4.3" }
1414
etherparse = { version = "0.17.0" }
1515
thiserror = { version = "2.0" }
16+
nom = { version = "8.0.0" }
1617

1718
p2p = { path = "../../p2p" }
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use std::fmt;
2+
3+
use nom::{
4+
bytes::complete::take,
5+
error::{Error, ErrorKind},
6+
number::complete::{be_u16, be_u8},
7+
Err, IResult,
8+
};
9+
10+
#[repr(u8)]
11+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12+
pub enum ContentType {
13+
ChangeCipherSpec = 20,
14+
Alert = 21,
15+
Handshake = 22,
16+
ApplicationData = 23,
17+
}
18+
19+
impl ContentType {
20+
pub fn from_u8(value: u8) -> Option<Self> {
21+
match value {
22+
20 => Some(ContentType::ChangeCipherSpec),
23+
21 => Some(ContentType::Alert),
24+
22 => Some(ContentType::Handshake),
25+
23 => Some(ContentType::ApplicationData),
26+
_ => None,
27+
}
28+
}
29+
}
30+
31+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32+
pub struct Chunk<'a> {
33+
pub ty: ContentType,
34+
pub epoch: u16,
35+
pub sequence_number: u64,
36+
pub length: u16,
37+
pub body: &'a [u8],
38+
}
39+
40+
impl fmt::Display for Chunk<'_> {
41+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42+
let Chunk {
43+
ty,
44+
epoch,
45+
sequence_number: seq,
46+
length,
47+
..
48+
} = self;
49+
write!(f, "{ty:?}, epoch={epoch}, seq={seq:012x}, len={length}")
50+
}
51+
}
52+
53+
impl<'a> Chunk<'a> {
54+
pub fn parse(input: &'a [u8]) -> IResult<&'a [u8], Self> {
55+
let (input, ty_byte) = be_u8(input)?;
56+
let ty = ContentType::from_u8(ty_byte)
57+
.ok_or_else(|| Err::Error(Error::new(input, ErrorKind::Alt)))?;
58+
59+
let (input, legacy_record_version) = be_u16(input)?;
60+
if legacy_record_version != 0xFEFD {
61+
return Err(Err::Error(Error::new(input, ErrorKind::Alt)));
62+
}
63+
64+
let (input, epoch) = be_u16(input)?;
65+
let (input, sequence_number_bytes) = take(6usize)(input)?;
66+
let sequence_number = {
67+
let mut buf = [0u8; 8];
68+
buf[2..].copy_from_slice(sequence_number_bytes);
69+
u64::from_be_bytes(buf)
70+
};
71+
let (input, length) = be_u16(input)?;
72+
let (input, body) = take(length as usize)(input)?;
73+
74+
let header = Chunk {
75+
ty,
76+
epoch,
77+
sequence_number,
78+
length,
79+
body,
80+
};
81+
82+
Ok((input, header))
83+
}
84+
}
85+
86+
#[cfg(test)]
87+
mod tests {
88+
use super::*;
89+
90+
#[test]
91+
fn test_parse_header() {
92+
let bytes: &[u8] = &[
93+
22, // ContentType::Handshake
94+
0xFE, 0xFD, // legacy_record_version (0xFEFD for DTLS 1.0)
95+
0x00, 0x01, // epoch
96+
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // sequence_number
97+
0x00, 0x13, // length
98+
];
99+
100+
let result = Chunk::parse(bytes);
101+
assert!(result.is_ok());
102+
let (_, header) = result.unwrap();
103+
104+
assert_eq!(header.ty, ContentType::Handshake);
105+
assert_eq!(header.epoch, 1);
106+
assert_eq!(header.sequence_number, 1);
107+
assert_eq!(header.length, 19);
108+
}
109+
}

tools/webrtc-sniffer/src/dtls/mod.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
mod header;
2+
3+
use nom::IResult;
4+
use p2p::identity::SecretKey;
5+
6+
use super::MsgHeader;
7+
8+
use self::header::Chunk;
9+
10+
pub struct State {
11+
secret_key: SecretKey,
12+
inner: Inner,
13+
}
14+
15+
enum Inner {
16+
Initial,
17+
RecvHello,
18+
}
19+
20+
impl State {
21+
pub fn new(secret_key: SecretKey) -> Self {
22+
State {
23+
secret_key,
24+
inner: Inner::Initial,
25+
}
26+
}
27+
28+
pub fn handle<'data>(
29+
&mut self,
30+
hrd: MsgHeader,
31+
mut data: &'data [u8],
32+
incoming: bool,
33+
) -> IResult<&'data [u8], ()> {
34+
let _ = incoming;
35+
loop {
36+
let (rest, chunk) = Chunk::parse(data)?;
37+
log::info!("{hrd} {chunk}");
38+
if rest.is_empty() {
39+
break Ok((rest, ()));
40+
} else {
41+
data = rest;
42+
}
43+
}
44+
}
45+
}

tools/webrtc-sniffer/src/lib.rs

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,71 @@
11
mod net;
22

3+
mod dtls;
4+
5+
use std::{borrow::Cow, collections::BTreeMap, fmt, net::SocketAddr};
6+
37
use p2p::identity::SecretKey;
48

59
use pcap::{Activated, Capture, Savefile};
610

11+
type State = dtls::State;
12+
13+
#[derive(Clone, Copy)]
14+
pub struct MsgHeader {
15+
src: SocketAddr,
16+
dst: SocketAddr,
17+
len: u16,
18+
}
19+
20+
impl fmt::Display for MsgHeader {
21+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22+
let MsgHeader { src, dst, len } = self;
23+
write!(f, "{src} -> {dst} {len}")
24+
}
25+
}
26+
727
pub fn run<T: Activated + ?Sized>(
828
capture: Capture<T>,
929
file: Option<Savefile>,
1030
secret_key: SecretKey,
1131
) -> Result<(), net::DissectError> {
12-
let _ = secret_key;
32+
let mut connections = BTreeMap::<(SocketAddr, SocketAddr), State>::new();
1333

34+
let mut buffer = None::<Vec<u8>>;
1435
for item in net::UdpIter::new(capture, file) {
1536
let (src, dst, data) = item?;
16-
log::info!("{src} -> {dst}: {} {}", data.len(), hex::encode(data));
17-
}
1837

19-
Ok(())
20-
}
38+
let hdr = MsgHeader {
39+
src,
40+
dst,
41+
len: data.len() as _,
42+
};
2143

22-
pub fn handle(packet: pcap::Packet) {
23-
use std::net::{IpAddr, SocketAddr};
24-
25-
use etherparse::{NetSlice, SlicedPacket, TransportSlice};
26-
27-
let eth = SlicedPacket::from_ethernet(packet.data).unwrap();
28-
if let (Some(net), Some(transport)) = (eth.net, eth.transport) {
29-
let (src_ip, dst_ip) = match net {
30-
NetSlice::Ipv4(ip) => (
31-
IpAddr::V4(ip.header().source().into()),
32-
IpAddr::V4(ip.header().destination().into()),
33-
),
34-
NetSlice::Ipv6(ip) => (
35-
IpAddr::V6(ip.header().source().into()),
36-
IpAddr::V6(ip.header().destination().into()),
37-
),
38-
NetSlice::Arp(_) => return,
44+
// skip STUN/TURN
45+
if data[4..8].eq(b"\x21\x12\xa4\x42") {
46+
continue;
47+
}
48+
49+
let data = if let Some(mut buffer) = buffer.take() {
50+
buffer.extend_from_slice(&data);
51+
Cow::Owned(buffer)
52+
} else {
53+
Cow::Borrowed(data.as_ref())
3954
};
40-
let (src_port, dst_port, slice) = match transport {
41-
TransportSlice::Udp(udp) => (udp.source_port(), udp.destination_port(), udp.payload()),
42-
_ => return,
55+
56+
let res = if let Some(cn) = connections.get_mut(&(src, dst)) {
57+
cn.handle(hdr, &data, true)
58+
} else {
59+
connections
60+
.entry((dst, src))
61+
.or_insert_with(|| State::new(secret_key.clone()))
62+
.handle(hdr, &data, false)
4363
};
4464

45-
let (src, dst, data) = (
46-
SocketAddr::new(src_ip, src_port),
47-
SocketAddr::new(dst_ip, dst_port),
48-
slice,
49-
);
50-
log::info!(
51-
"{src} -> {dst}: {} {}",
52-
data.len(),
53-
hex::encode(&data[..data.len().min(12)])
54-
);
65+
if let Err(nom::Err::Incomplete(_)) = res {
66+
buffer = Some(data.into_owned());
67+
}
5568
}
69+
70+
Ok(())
5671
}

0 commit comments

Comments
 (0)