Skip to main content

wireforge_app/
app_packet.rs

1//! Application-layer packet dispatch for WireForge.
2//!
3//! This module provides a unified [`AppPacket`] enum covering the
4//! application-layer protocols added in v1.1 and v1.2, plus helper functions
5//! to dispatch a TCP or UDP payload to the correct parser based on the
6//! well-known port.
7
8use crate::llmnr::LlmnrPacket;
9use crate::mdns::MdnsPacket;
10use crate::modbus::ModbusTcpPacket;
11use crate::mqtt::MqttPacket;
12use crate::netbios::NetbiosNsPacket;
13use crate::radius::RadiusPacket;
14use crate::rdp::TpktHeader;
15use crate::smb2::Smb2Header;
16use crate::ssh::SshPacket;
17use crate::tls::TlsClientHello;
18
19/// Standard IANA ports used for application-layer dispatch.
20mod ports {
21    pub const TLS: u16 = 443;
22    pub const SSH: u16 = 22;
23    pub const MQTT: u16 = 1883;
24    pub const MODBUS: u16 = 502;
25    pub const SMB2: u16 = 445;
26    pub const RDP: u16 = 3389;
27    pub const RADIUS_AUTH: u16 = 1812;
28    pub const RADIUS_ACCT: u16 = 1813;
29    pub const MDNS: u16 = 5353;
30    pub const LLMNR: u16 = 5355;
31    pub const NETBIOS_NS: u16 = 137;
32}
33
34/// Unified application-layer packet enum covering v1.1 and v1.2 protocols.
35#[non_exhaustive]
36#[derive(Debug, Clone)]
37pub enum AppPacket<'a> {
38    Tls(TlsClientHello<'a>),
39    Ssh(SshPacket<'a>),
40    Mqtt(MqttPacket<'a>),
41    Modbus(ModbusTcpPacket<'a>),
42    Radius(RadiusPacket<'a>),
43    Mdns(MdnsPacket<'a>),
44    Llmnr(LlmnrPacket<'a>),
45    Netbios(NetbiosNsPacket<'a>),
46    Smb2(Smb2Header<'a>),
47    Rdp(TpktHeader<'a>),
48}
49
50impl<'a> AppPacket<'a> {
51    /// Human-readable protocol name.
52    pub fn protocol_name(&self) -> &'static str {
53        match self {
54            AppPacket::Tls(_) => "TLS",
55            AppPacket::Ssh(_) => "SSH",
56            AppPacket::Mqtt(_) => "MQTT",
57            AppPacket::Modbus(_) => "ModbusTCP",
58            AppPacket::Radius(_) => "RADIUS",
59            AppPacket::Mdns(_) => "mDNS",
60            AppPacket::Llmnr(_) => "LLMNR",
61            AppPacket::Netbios(_) => "NetBIOS-NS",
62            AppPacket::Smb2(_) => "SMB2",
63            AppPacket::Rdp(_) => "RDP",
64        }
65    }
66}
67
68macro_rules! from_impl {
69    ($variant:ident, $type:ty) => {
70        impl<'a> From<$type> for AppPacket<'a> {
71            fn from(p: $type) -> Self {
72                AppPacket::$variant(p)
73            }
74        }
75    };
76}
77
78from_impl!(Tls, TlsClientHello<'a>);
79from_impl!(Ssh, SshPacket<'a>);
80from_impl!(Mqtt, MqttPacket<'a>);
81from_impl!(Modbus, ModbusTcpPacket<'a>);
82from_impl!(Radius, RadiusPacket<'a>);
83from_impl!(Mdns, MdnsPacket<'a>);
84from_impl!(Llmnr, LlmnrPacket<'a>);
85from_impl!(Netbios, NetbiosNsPacket<'a>);
86from_impl!(Smb2, Smb2Header<'a>);
87from_impl!(Rdp, TpktHeader<'a>);
88
89#[inline]
90fn port_matches(src: u16, dst: u16, port: u16) -> bool {
91    src == port || dst == port
92}
93
94/// Dispatch a TCP payload to an application-layer parser based on port numbers.
95///
96/// Only the standard ports for TLS, SSH, MQTT, Modbus TCP, SMB2, and RDP are
97/// recognized. The payload is parsed only when the port matches; if parsing
98/// fails, `None` is returned.
99///
100/// # Example
101///
102/// ```rust,ignore
103/// use wireforge_app::app_packet::parse_tcp_application;
104///
105/// let pkt = parse_tcp_application(&buf, 54321, 443).expect("TLS packet");
106/// println!("{}", pkt.protocol_name());
107/// ```
108pub fn parse_tcp_application(buf: &[u8], src_port: u16, dst_port: u16) -> Option<AppPacket<'_>> {
109    if port_matches(src_port, dst_port, ports::TLS) {
110        return TlsClientHello::parse(buf).map(AppPacket::Tls);
111    }
112    if port_matches(src_port, dst_port, ports::SSH) {
113        return SshPacket::parse(buf).map(AppPacket::Ssh);
114    }
115    if port_matches(src_port, dst_port, ports::MQTT) {
116        return MqttPacket::parse(buf).map(AppPacket::Mqtt);
117    }
118    if port_matches(src_port, dst_port, ports::MODBUS) {
119        return ModbusTcpPacket::new(buf).map(AppPacket::Modbus);
120    }
121    if port_matches(src_port, dst_port, ports::SMB2) {
122        return Smb2Header::parse(buf).map(AppPacket::Smb2);
123    }
124    if port_matches(src_port, dst_port, ports::RDP) {
125        return TpktHeader::parse(buf).map(AppPacket::Rdp);
126    }
127    None
128}
129
130/// Dispatch a UDP payload to an application-layer parser based on port numbers.
131///
132/// Recognizes RADIUS, mDNS, LLMNR, and NetBIOS-NS on their standard ports.
133pub fn parse_udp_application(buf: &[u8], src_port: u16, dst_port: u16) -> Option<AppPacket<'_>> {
134    if port_matches(src_port, dst_port, ports::RADIUS_AUTH)
135        || port_matches(src_port, dst_port, ports::RADIUS_ACCT)
136    {
137        return RadiusPacket::new(buf).map(AppPacket::Radius);
138    }
139    if port_matches(src_port, dst_port, ports::MDNS) {
140        return MdnsPacket::new(buf).map(AppPacket::Mdns);
141    }
142    if port_matches(src_port, dst_port, ports::LLMNR) {
143        return LlmnrPacket::new(buf).map(AppPacket::Llmnr);
144    }
145    if port_matches(src_port, dst_port, ports::NETBIOS_NS) {
146        return NetbiosNsPacket::new(buf).map(AppPacket::Netbios);
147    }
148    None
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::dns::builder::DnsPacketBuilder;
155    use crate::dns::types::{DnsClass, DnsType};
156    use crate::mqtt::MqttPacketBuilder;
157
158    #[test]
159    fn dispatch_tls_on_443() {
160        let data = &[
161            0x16, 0x03, 0x01, 0x00, 0x44, // record: handshake, len=68
162            0x01, 0x00, 0x00, 0x40,       // handshake: client_hello, len=64
163            0x03, 0x03,                   // client_version TLS 1.2 (legacy)
164            // random (32 bytes)
165            0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,
166            0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,0xAA,
167            0x00,                         // session_id_len = 0
168            0x00, 0x02, 0x13, 0x01,       // cipher_suites: TLS_AES_128_GCM
169            0x01, 0x00,                   // compression: null
170            0x00, 0x07,                   // extensions length = 7
171            0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04, // supported_versions: TLS 1.3
172        ];
173        let pkt = parse_tcp_application(data, 54321, 443).unwrap();
174        assert!(matches!(pkt, AppPacket::Tls(_)));
175        assert_eq!(pkt.protocol_name(), "TLS");
176    }
177
178    #[test]
179    fn dispatch_ssh_on_22() {
180        let data = b"SSH-2.0-OpenSSH_8.9\r\n";
181        let pkt = parse_tcp_application(data, 22, 54321).unwrap();
182        assert!(matches!(pkt, AppPacket::Ssh(SshPacket::Banner(_))));
183        assert_eq!(pkt.protocol_name(), "SSH");
184    }
185
186    #[test]
187    fn dispatch_mqtt_on_1883() {
188        let data = MqttPacketBuilder::connect("sensor1");
189        let pkt = parse_tcp_application(&data, 1883, 12345).unwrap();
190        assert!(matches!(pkt, AppPacket::Mqtt(_)));
191    }
192
193    #[test]
194    fn dispatch_modbus_on_502() {
195        let data = &[0x00u8, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x0A];
196        let pkt = parse_tcp_application(data, 502, 12345).unwrap();
197        assert!(matches!(pkt, AppPacket::Modbus(_)));
198    }
199
200    #[test]
201    fn dispatch_smb2_on_445() {
202        let mut hdr = [0u8; 64];
203        hdr[4..8].copy_from_slice(b"\xfeSMB");
204        let pkt = parse_tcp_application(&hdr, 445, 12345).unwrap();
205        assert!(matches!(pkt, AppPacket::Smb2(_)));
206    }
207
208    #[test]
209    fn dispatch_rdp_on_3389() {
210        let data = [0x03u8, 0x00, 0x00, 0x2C, 0xE0];
211        let pkt = parse_tcp_application(&data, 3389, 12345).unwrap();
212        assert!(matches!(pkt, AppPacket::Rdp(_)));
213    }
214
215    #[test]
216    fn dispatch_radius_on_1812() {
217        let pkt = vec![1u8, 0, 0, 20, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
218        let parsed = parse_udp_application(&pkt, 1812, 12345).unwrap();
219        assert!(matches!(parsed, AppPacket::Radius(_)));
220    }
221
222    #[test]
223    fn dispatch_mdns_on_5353() {
224        let q = DnsPacketBuilder::query()
225            .add_question("_http._tcp.local", DnsType::PTR, DnsClass::IN)
226            .build();
227        let pkt = parse_udp_application(&q, 5353, 12345).unwrap();
228        assert!(matches!(pkt, AppPacket::Mdns(_)));
229    }
230
231    #[test]
232    fn dispatch_llmnr_on_5355() {
233        let q = DnsPacketBuilder::query()
234            .add_question("wpad", DnsType::A, DnsClass::IN)
235            .build();
236        let pkt = parse_udp_application(&q, 12345, 5355).unwrap();
237        assert!(matches!(pkt, AppPacket::Llmnr(_)));
238    }
239
240    #[test]
241    fn dispatch_netbios_on_137() {
242        let mut data = vec![0x00, 0x01, 0x01, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
243        let mut enc = [0u8; 32];
244        let name = b"HOST1           ";
245        for (i, b) in name.iter().enumerate() {
246            enc[i * 2] = (b >> 4) + 0x41;
247            enc[i * 2 + 1] = (b & 0x0F) + 0x41;
248        }
249        data.extend_from_slice(&enc);
250        data.extend_from_slice(&[0x00, 0x20, 0x00, 0x01]);
251        let pkt = parse_udp_application(&data, 137, 12345).unwrap();
252        assert!(matches!(pkt, AppPacket::Netbios(_)));
253    }
254
255    #[test]
256    fn unknown_tcp_port_returns_none() {
257        assert!(parse_tcp_application(b"foo", 12345, 54321).is_none());
258    }
259
260    #[test]
261    fn unknown_udp_port_returns_none() {
262        assert!(parse_udp_application(b"foo", 12345, 54321).is_none());
263    }
264
265    #[test]
266    fn known_port_bad_data_returns_none() {
267        assert!(parse_tcp_application(b"not tls", 443, 12345).is_none());
268        assert!(parse_udp_application(&[0u8; 10], 1812, 12345).is_none());
269    }
270}