use crate::{MacAddress, error::FormatError};
#[derive(Debug, Clone, Copy)]
pub enum PingOutcome {
HostDidntRespondToPingMethods { tcp: bool, icmp: bool },
IcmpEchoPacket,
HostLocalNetworkDidntRespondArpWhoIsQuery,
ArpWhoIsQuery(MacAddress),
IcmpUnreachPacketInResponseToTcpSynPacket { to: u16 },
RepliedTcpSynPacketWithRstAck { to: u16 },
RepliedTcpSynPacketWithSynAck { to: u16 },
RepliedTcpSynPacketWithFinAck { to: u16 },
EmittedTcpSynPacket { from: u16, to: u16 },
EmittedTcpAckPacket { from: u16, to: u16 },
EmittedTcpPushAckPacket { from: u16, to: u16 },
IcmpUnreachPacket,
EmittedUdpPacket { from: u16, to: u16 },
LocalScanner,
}
impl PingOutcome {
#[allow(clippy::too_many_lines)]
pub(crate) fn from_plugin_output(plugin_output: &str) -> Result<Self, FormatError> {
const LOCAL_SCANNER: &str = "The remote host is up\nThe host is the local scanner.";
const ICMP_ECHO_PACKET: &str =
"The remote host is up\nThe remote host replied to an ICMP echo packet";
const ICMP_UNREACH_PACKET: &str =
"The remote host is up\nThe remote host replied with an ICMP unreach packet.";
const ARP_WHO_IS_QUERY_PREFIX: &str =
"The remote host is up\nThe host replied to an ARP who-is query.\nHardware address : ";
const REPLIED_TCP_SYN_PACKET_PREFIX: &str =
"The remote host is up\nThe remote host replied to a TCP SYN packet sent to port ";
const WITH_RST_ACK_SUFFIX: &str = " with a RST,ACK packet";
const WITH_SYN_ACK_SUFFIX: &str = " with a SYN,ACK packet";
const WITH_FIN_ACK_SUFFIX: &str = " with a FIN,ACK packet";
const ICMP_UNREACH_PACKET_RESPONSE_TO_TCP_SYN_PACKET_PREFIX: &str = "The remote host is up\nThe remote host replied with an ICMP unreach packet sent in response to a TCP SYN packet sent to port ";
const EMITTED_UDP_PACKET_PREFIX: &str =
"The remote host is up\nThe remote host emited a UDP packet from port ";
const EMITTED_PACKET_MIDDLE: &str = "going to port ";
const EMITTED_TCP_SYN_PACKET_PREFIX: &str =
"The remote host is up\nThe remote host emitted a TCP SYN packet from port ";
const EMITTED_TCP_ACK_PACKET_PREFIX: &str =
"The remote host is up\nThe remote host emitted a TCP ACK packet from port ";
const EMITTED_TCP_PUSH_ACK_PACKET_PREFIX: &str =
"The remote host is up\nThe remote host emitted a TCP PUSH,ACK packet from port ";
const DEAD_HOST_NEEDLE: &str = ") is considered as dead - not scanning\nThe remote host (";
const FAILED_TO_REPLY_ARP_SUFFIX: &str =
") is on the local network and failed to reply to an ARP who-is query.";
const DIDNT_RESPOND_TO_PING_METHODS_NEEDLE: &str =
") did not respond to the following ping methods :\n";
match plugin_output {
ICMP_ECHO_PACKET => Ok(Self::IcmpEchoPacket),
LOCAL_SCANNER => Ok(Self::LocalScanner),
ICMP_UNREACH_PACKET => Ok(Self::IcmpUnreachPacket),
text => {
if text.contains(DEAD_HOST_NEEDLE) {
if text.ends_with(FAILED_TO_REPLY_ARP_SUFFIX) {
Ok(Self::HostLocalNetworkDidntRespondArpWhoIsQuery)
} else if let Some((_, ping_methods)) =
text.split_once(DIDNT_RESPOND_TO_PING_METHODS_NEEDLE)
{
let mut tcp = false;
let mut icmp = false;
for ping_method in ping_methods.trim_end().split('\n') {
match ping_method {
"- TCP ping" => tcp = true,
"- ICMP ping" => icmp = true,
_ => {
return Err(FormatError::UnexpectedPingMethod(
plugin_output.into(),
));
}
}
}
if !tcp && !icmp {
Err(FormatError::UnexpectedPingFormat(plugin_output.into()))
} else {
Ok(Self::HostDidntRespondToPingMethods { tcp, icmp })
}
} else {
Err(FormatError::UnexpectedDeadHostReason(plugin_output.into()))
}
} else if let Some(mac) = text.strip_prefix(ARP_WHO_IS_QUERY_PREFIX) {
Ok(Self::ArpWhoIsQuery(mac.parse()?))
} else if let Some(port) =
text.strip_prefix(ICMP_UNREACH_PACKET_RESPONSE_TO_TCP_SYN_PACKET_PREFIX)
{
Ok(Self::IcmpUnreachPacketInResponseToTcpSynPacket { to: port.parse()? })
} else if let Some(port_and_suffix) =
text.strip_prefix(REPLIED_TCP_SYN_PACKET_PREFIX)
{
if let Some(port) = port_and_suffix.strip_suffix(WITH_RST_ACK_SUFFIX) {
Ok(Self::RepliedTcpSynPacketWithRstAck { to: port.parse()? })
} else if let Some(port) = port_and_suffix.strip_suffix(WITH_SYN_ACK_SUFFIX) {
Ok(Self::RepliedTcpSynPacketWithSynAck { to: port.parse()? })
} else if let Some(port) = port_and_suffix.strip_suffix(WITH_FIN_ACK_SUFFIX) {
Ok(Self::RepliedTcpSynPacketWithFinAck { to: port.parse()? })
} else {
Err(FormatError::UnexpectedPingTcpResponse(plugin_output.into()))
}
} else if let Some(from_port_and_rest) =
text.strip_prefix(EMITTED_TCP_SYN_PACKET_PREFIX)
&& let Some((from_port, rest)) = from_port_and_rest.split_once(' ')
&& let Some(to_port) = rest.strip_prefix(EMITTED_PACKET_MIDDLE)
{
Ok(Self::EmittedTcpSynPacket {
from: from_port.parse()?,
to: to_port.parse()?,
})
} else if let Some(from_port_and_rest) =
text.strip_prefix(EMITTED_TCP_ACK_PACKET_PREFIX)
&& let Some((from_port, rest)) = from_port_and_rest.split_once(' ')
&& let Some(to_port) = rest.strip_prefix(EMITTED_PACKET_MIDDLE)
{
Ok(Self::EmittedTcpAckPacket {
from: from_port.parse()?,
to: to_port.parse()?,
})
} else if let Some(from_port_and_rest) =
text.strip_prefix(EMITTED_TCP_PUSH_ACK_PACKET_PREFIX)
&& let Some((from_port, rest)) = from_port_and_rest.split_once(' ')
&& let Some(to_port) = rest.strip_prefix(EMITTED_PACKET_MIDDLE)
{
Ok(Self::EmittedTcpPushAckPacket {
from: from_port.parse()?,
to: to_port.parse()?,
})
} else if let Some(from_port_and_rest) =
text.strip_prefix(EMITTED_UDP_PACKET_PREFIX)
&& let Some((from_port, rest)) = from_port_and_rest.split_once(' ')
&& let Some(to_port) = rest.strip_prefix(EMITTED_PACKET_MIDDLE)
{
Ok(Self::EmittedUdpPacket {
from: from_port.parse()?,
to: to_port.parse()?,
})
} else {
Err(FormatError::UnexpectedPingFormat(plugin_output.into()))
}
}
}
}
}
#[cfg(test)]
mod tests {
use crate::error::FormatError;
use super::PingOutcome;
#[test]
fn parses_common_ping_outcomes() {
let icmp_echo = "The remote host is up\nThe remote host replied to an ICMP echo packet";
assert!(matches!(
PingOutcome::from_plugin_output(icmp_echo).expect("must parse"),
PingOutcome::IcmpEchoPacket
));
let local = "The remote host is up\nThe host is the local scanner.";
assert!(matches!(
PingOutcome::from_plugin_output(local).expect("must parse"),
PingOutcome::LocalScanner
));
let arp = "The remote host is up\nThe host replied to an ARP who-is query.\nHardware address : 0a:1b:2c:3d:4e:5f";
assert!(matches!(
PingOutcome::from_plugin_output(arp).expect("must parse"),
PingOutcome::ArpWhoIsQuery(_)
));
}
#[test]
fn parses_tcp_and_udp_based_outcomes() {
let syn_ack = "The remote host is up\nThe remote host replied to a TCP SYN packet sent to port 443 with a SYN,ACK packet";
assert!(matches!(
PingOutcome::from_plugin_output(syn_ack).expect("must parse"),
PingOutcome::RepliedTcpSynPacketWithSynAck { to: 443 }
));
let emitted_udp = "The remote host is up\nThe remote host emited a UDP packet from port 68 going to port 67";
assert!(matches!(
PingOutcome::from_plugin_output(emitted_udp).expect("must parse"),
PingOutcome::EmittedUdpPacket { from: 68, to: 67 }
));
}
#[test]
fn parses_dead_host_ping_methods() {
let text = "The remote host (1.2.3.4) is considered as dead - not scanning\nThe remote host (1.2.3.4) did not respond to the following ping methods :\n- TCP ping\n- ICMP ping";
assert!(matches!(
PingOutcome::from_plugin_output(text).expect("must parse"),
PingOutcome::HostDidntRespondToPingMethods {
tcp: true,
icmp: true
}
));
}
#[test]
fn rejects_unexpected_ping_method_lines() {
let text = "The remote host (1.2.3.4) is considered as dead - not scanning\nThe remote host (1.2.3.4) did not respond to the following ping methods :\n- UDP ping";
let err = PingOutcome::from_plugin_output(text).expect_err("must fail");
assert!(matches!(err, FormatError::UnexpectedPingMethod(_)));
}
#[test]
fn rejects_unknown_dead_host_reason() {
let text = "The remote host (1.2.3.4) is considered as dead - not scanning\nThe remote host (1.2.3.4) failed for an unknown reason.";
let err = PingOutcome::from_plugin_output(text).expect_err("must fail");
assert!(matches!(err, FormatError::UnexpectedDeadHostReason(_)));
}
#[test]
fn rejects_unexpected_tcp_reply_suffix() {
let text = "The remote host is up\nThe remote host replied to a TCP SYN packet sent to port 443 with a MAYBE packet";
let err = PingOutcome::from_plugin_output(text).expect_err("must fail");
assert!(matches!(err, FormatError::UnexpectedPingTcpResponse(_)));
}
#[test]
fn rejects_unrecognized_ping_format() {
let text = "completely unexpected";
let err = PingOutcome::from_plugin_output(text).expect_err("must fail");
assert!(matches!(err, FormatError::UnexpectedPingFormat(_)));
}
}