For those of you who saw the word “Rust” in the title, don't be alarmed. I don't want to start yet another holy war about which language is better. I'm here for some design advice.
Recently, I set myself the goal of writing a console-based Internet packet monitor. To my surprise, I found very few C++ projects whose source code I could study. But I did find one project that I liked, and that is https://github.com/matthart1983/netwatch. It’s written in Rust and is quite well-written (I don’t know Rust, but even I was able to understand some parts of the code, this is how I define "well-written").
One part of the code caught my attention more than the others. This part is located in the dpi folder. https://github.com/matthart1983/netwatch/tree/main/src/dpi
Essentially, the main things we have are the following:
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AppProtocol {
/// DNS query or response — first question's qname + qtype, plus the
/// header RCODE on responses.
Dns {
qname: String,
qtype: u16,
/// Response code from the DNS header (`0`=NOERROR, `3`=NXDOMAIN,
/// …). `Some` only when the QR bit marks this a response; `None`
/// for queries.
#[serde(default)]
rcode: Option<u8>,
},
I might be wrong, but I think it's called ADT (if I'm wrong, please forgive me—I'll correct the title of this post).
Another part is this
pub trait Classifier {
/// `Some(protocol)` when this classifier recognizes the payload,
/// `None` to fall through to the next classifier.
fn classify(&self, payload: &[u8], is_tcp: bool) -> Option<AppProtocol>;
}
And finally, here are the following sections for each of the protocols:
pub struct DnsClassifier;
impl Classifier for DnsClassifier {
fn classify(&self, payload: &[u8], is_tcp: bool) -> Option<AppProtocol> {
if is_tcp {
return None; // DNS-over-TCP exists; skip for now.
}
if payload.len() < HEADER_LEN + 1 + 4 {
return None; // minimum: header + root qname(1) + qtype+qclass(4)
}
let qdcount = u16::from_be_bytes([payload[4], payload[5]]);
if qdcount == 0 {
return None;
}
// Header flags: QR is the high bit of byte 2; RCODE is the low
// nibble of byte 3 (RFC 1035 §4.1.1). We surface the response code
// only on responses (QR=1); queries report `None`.
let is_response = payload[2] & 0x80 != 0;
let rcode = if is_response {
Some(payload[3] & 0x0F)
} else {
None
};
// ... some lines here
My question is this: What would the design of the classes/structures look like if we were writing all of this in C++?
I'm curious to see what elegant, clean code for exactly the same task might look like in C++.
The first thought that came to mind was to simply write a protocol interface that included a `classify` method.
class Protocol {
public:
virtual ~Protocol() = default;
// note: return type of this function is void
// i don't know how to do it better
// maybe std::optional?
virtual void classify(std::span<std::byte> payload, bool is_tcp);
}
Beyond that, we simply have a bunch of derived classes for each protocol.
class DnsProtocol : public Protocol {
public:
constexpr auto HEADER_LEN = 12u;
virtual void classify(std::span<std::byte> payload, bool is_tcp) {
if (is_tcp) return;
if (payload.size() < HEADER_LEN + 1 + 4) {
return; // minimum: header + root qname(1) + qtype+qclass(4)
}
}
}
What do you think?