Scanning

Scanning

Probes are responsible for three things: writing packets for a target, parsing responses, and attaching metadata tags. Every probe in scan/src/probe derives from the same traits.

Core traits

  • ProbeParser (defined in scan/src/probe/mod.rs) drives packet construction and parsing. Implement the following methods:
    • write_transport(&self, packet, src_addr) should fill a transport-layer header and return the protocol plus byte count.
    • set_target_transport(&self, packet, src, dst) finalises checksums before transmission.
    • parse_transport(protocol, packet) extracts your result type from a response.
  • ScanTagProvider lets your result decorate ScanResult objects with strongly-typed tags. Return a Vec<ScanResultTag> describing the metadata you captured.
#[derive(Debug, Clone)]
pub struct FooReply {
    pub field: u16,
}

impl ScanTagProvider for FooReply {
    fn tags(&self) -> Vec<ScanResultTag> {
        vec![ScanResultTag::Custom {
            key: "foo_field".to_string(),
            value: self.field.to_string(),
        }]
    }
}

#[derive(clap::Args, Clone, Serialize, Deserialize, Default)]
pub struct FooProbe {
    #[arg(long, default_value = "1234")]
    pub port: u16,
}

impl ProbeParser for FooProbe {
    type Result = FooReply;

    fn write_transport(&self, packet: &mut [u8], _src: IpAddr) -> Option<(IpNextHeaderProtocol, usize)> {
        let mut udp = MutableUdpPacket::new(packet)?;
        udp.set_source(54321);
        udp.set_destination(self.port);
        udp.set_length(8);
        udp.set_checksum(0);
        Some((IpNextHeaderProtocols::Udp, 8))
    }

    fn set_target_transport(&self, packet: &mut [u8], src: IpAddr, dst: IpAddr) {
        let mut udp = MutableUdpPacket::new(packet).unwrap();
        let sum = match (src, dst) {
            (IpAddr::V4(s), IpAddr::V4(d)) => pnet::packet::udp::ipv4_checksum(&udp.to_immutable(), &s, &d),
            (IpAddr::V6(s), IpAddr::V6(d)) => pnet::packet::udp::ipv6_checksum(&udp.to_immutable(), &s, &d),
            _ => unreachable!(),
        };
        udp.set_checksum(sum);
    }

    fn parse_transport(protocol: IpNextHeaderProtocol, packet: &[u8]) -> Option<Self::Result> {
        if protocol == IpNextHeaderProtocols::Udp {
            let udp = pnet::packet::udp::UdpPacket::new(packet)?;
            Some(FooReply { field: udp.get_source() })
        } else {
            None
        }
    }
}

Registering the probe

  1. Create a module in scan/src/probe/ (or extend an existing one) that defines your ProbeParser implementation and result type.
  2. Add a new variant to ProbeConfig in scan/src/lib.rs, extend the spawn_probe match, and update the Clap documentation comment.
  3. Expose factory functions in the Python bindings (pyrmap/src/lib.rs) so scripting workflows can opt into the new probe.
  4. Document configuration flags under site/content/docs/scanning/ so the sidebar stays in sync.

Tips

  • Probes operate on raw packets. Allocate buffers large enough for Ethernet + IPv4/IPv6 + transport headers when calling write_ethernet.
  • Keep per-target state inside your Result type if you need to correlate replies; results are processed after dealiasing completes.
  • Use ScanTagProvider to surface structured data—downstream CSV exports and Python bindings consume these tags automatically.