diff --git a/ntp-proto/src/packet/mod.rs b/ntp-proto/src/packet/mod.rs index 71f73995f..313ef87cd 100644 --- a/ntp-proto/src/packet/mod.rs +++ b/ntp-proto/src/packet/mod.rs @@ -858,7 +858,25 @@ impl<'a> NtpPacket<'a> { mac: None, }, #[cfg(feature = "ntpv5")] - NtpHeader::V5(_header) => todo!("NTPv5 does not have KISS codes yet"), + NtpHeader::V5(header) => NtpPacket { + header: NtpHeader::V5(v5::NtpHeaderV5::rate_limit_response(header)), + efdata: ExtensionFieldData { + authenticated: vec![], + encrypted: vec![], + // Ignore encrypted so as not to accidentaly leak anything + untrusted: packet_from_client + .efdata + .untrusted + .into_iter() + .chain(packet_from_client.efdata.authenticated) + .filter(|ef| matches!(ef, ExtensionField::UniqueIdentifier(_))) + .chain(std::iter::once(ExtensionField::DraftIdentification( + Cow::Borrowed(v5::DRAFT_VERSION), + ))) + .collect(), + }, + mac: None, + }, } } @@ -880,7 +898,23 @@ impl<'a> NtpPacket<'a> { mac: None, }, #[cfg(feature = "ntpv5")] - NtpHeader::V5(_header) => todo!("No NTS support yet"), + NtpHeader::V5(header) => NtpPacket { + header: NtpHeader::V5(v5::NtpHeaderV5::rate_limit_response(header)), + efdata: ExtensionFieldData { + authenticated: packet_from_client + .efdata + .authenticated + .into_iter() + .filter(|ef| matches!(ef, ExtensionField::UniqueIdentifier(_))) + .chain(std::iter::once(ExtensionField::DraftIdentification( + Cow::Borrowed(v5::DRAFT_VERSION), + ))) + .collect(), + encrypted: vec![], + untrusted: vec![], + }, + mac: None, + }, } } @@ -908,7 +942,25 @@ impl<'a> NtpPacket<'a> { mac: None, }, #[cfg(feature = "ntpv5")] - NtpHeader::V5(_header) => todo!("NTPv5 does not have KISS codes yet"), + NtpHeader::V5(header) => NtpPacket { + header: NtpHeader::V5(v5::NtpHeaderV5::deny_response(header)), + efdata: ExtensionFieldData { + authenticated: vec![], + encrypted: vec![], + // Ignore encrypted so as not to accidentaly leak anything + untrusted: packet_from_client + .efdata + .untrusted + .into_iter() + .chain(packet_from_client.efdata.authenticated) + .filter(|ef| matches!(ef, ExtensionField::UniqueIdentifier(_))) + .chain(std::iter::once(ExtensionField::DraftIdentification( + Cow::Borrowed(v5::DRAFT_VERSION), + ))) + .collect(), + }, + mac: None, + }, } } @@ -930,7 +982,23 @@ impl<'a> NtpPacket<'a> { mac: None, }, #[cfg(feature = "ntpv5")] - NtpHeader::V5(_header) => todo!("No NTS support for NTPv5 yet"), + NtpHeader::V5(header) => NtpPacket { + header: NtpHeader::V5(v5::NtpHeaderV5::deny_response(header)), + efdata: ExtensionFieldData { + authenticated: packet_from_client + .efdata + .authenticated + .into_iter() + .filter(|ef| matches!(ef, ExtensionField::UniqueIdentifier(_))) + .chain(std::iter::once(ExtensionField::DraftIdentification( + Cow::Borrowed(v5::DRAFT_VERSION), + ))) + .collect(), + encrypted: vec![], + untrusted: vec![], + }, + mac: None, + }, } } @@ -953,7 +1021,24 @@ impl<'a> NtpPacket<'a> { mac: None, }, #[cfg(feature = "ntpv5")] - NtpHeader::V5(_header) => todo!("No NTS support for NTPv5 yet"), + NtpHeader::V5(header) => NtpPacket { + header: NtpHeader::V5(v5::NtpHeaderV5::nts_nak_response(header)), + efdata: ExtensionFieldData { + authenticated: vec![], + encrypted: vec![], + untrusted: packet_from_client + .efdata + .untrusted + .into_iter() + .chain(packet_from_client.efdata.authenticated) + .filter(|ef| matches!(ef, ExtensionField::UniqueIdentifier(_))) + .chain(std::iter::once(ExtensionField::DraftIdentification( + Cow::Borrowed(v5::DRAFT_VERSION), + ))) + .collect(), + }, + mac: None, + }, } } } @@ -1074,30 +1159,41 @@ impl<'a> NtpPacket<'a> { } } + fn kiss_code(&self) -> ReferenceId { + match self.header { + NtpHeader::V3(header) => header.reference_id, + NtpHeader::V4(header) => header.reference_id, + #[cfg(feature = "ntpv5")] + // Kiss code in ntpv5 is the first four bytes of the server cookie + NtpHeader::V5(header) => { + ReferenceId::from_bytes(header.server_cookie.0[..4].try_into().unwrap()) + } + } + } + pub fn is_kiss(&self) -> bool { match self.header { NtpHeader::V3(header) => header.stratum == 0, NtpHeader::V4(header) => header.stratum == 0, #[cfg(feature = "ntpv5")] - // TODO NTPv5 does not have Kiss codes so we pretend everything is always fine - NtpHeader::V5(_header) => false, + NtpHeader::V5(header) => header.flags.status_message, } } pub fn is_kiss_deny(&self) -> bool { - self.is_kiss() && self.reference_id().is_deny() + self.is_kiss() && self.kiss_code().is_deny() } pub fn is_kiss_rate(&self) -> bool { - self.is_kiss() && self.reference_id().is_rate() + self.is_kiss() && self.kiss_code().is_rate() } pub fn is_kiss_rstr(&self) -> bool { - self.is_kiss() && self.reference_id().is_rstr() + self.is_kiss() && self.kiss_code().is_rstr() } pub fn is_kiss_ntsn(&self) -> bool { - self.is_kiss() && self.reference_id().is_ntsn() + self.is_kiss() && self.kiss_code().is_ntsn() } #[cfg(feature = "ntpv5")] diff --git a/ntp-proto/src/packet/v5/mod.rs b/ntp-proto/src/packet/v5/mod.rs index db6235b78..223c53dc7 100644 --- a/ntp-proto/src/packet/v5/mod.rs +++ b/ntp-proto/src/packet/v5/mod.rs @@ -77,19 +77,21 @@ pub struct NtpEra(pub u8); #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct NtpFlags { - unknown_leap: bool, - interleaved_mode: bool, + pub unknown_leap: bool, + pub interleaved_mode: bool, + pub status_message: bool, } impl NtpFlags { const fn from_bits(bits: [u8; 2]) -> Result> { - if bits[0] != 0x00 || bits[1] & 0b1111_1100 != 0 { + if bits[0] != 0x00 || bits[1] & 0b1111_1000 != 0 { return Err(V5Error::InvalidFlags.into_parse_err()); } Ok(Self { unknown_leap: bits[1] & 0b01 != 0, interleaved_mode: bits[1] & 0b10 != 0, + status_message: bits[1] & 0b100 != 0, }) } @@ -104,6 +106,10 @@ impl NtpFlags { flags |= 0b10; } + if self.status_message { + flags |= 0b100; + } + [0x00, flags] } } @@ -173,6 +179,7 @@ impl NtpHeaderV5 { flags: NtpFlags { unknown_leap: false, interleaved_mode: false, + status_message: false, }, server_cookie: NtpServerCookie([0; 8]), client_cookie: NtpClientCookie([0; 8]), @@ -200,6 +207,7 @@ impl NtpHeaderV5 { flags: NtpFlags { unknown_leap: false, interleaved_mode: false, + status_message: false, }, root_delay: system.time_snapshot.root_delay, root_dispersion: system.time_snapshot.root_dispersion, @@ -210,6 +218,35 @@ impl NtpHeaderV5 { } } + fn kiss_response(packet_from_client: Self, code: [u8; 4]) -> Self { + Self { + mode: NtpMode::Response, + flags: NtpFlags { + unknown_leap: false, + interleaved_mode: false, + status_message: true, + }, + server_cookie: NtpServerCookie([code[0], code[1], code[2], code[3], 0, 0, 0, 0]), + client_cookie: packet_from_client.client_cookie, + ..Self::new() + } + } + + pub(crate) fn rate_limit_response(packet_from_client: Self) -> Self { + Self { + poll: packet_from_client.poll.force_inc(), + ..Self::kiss_response(packet_from_client, *b"RATE") + } + } + + pub(crate) fn deny_response(packet_from_client: Self) -> Self { + Self::kiss_response(packet_from_client, *b"DENY") + } + + pub(crate) fn nts_nak_response(packet_from_client: Self) -> Self { + Self::kiss_response(packet_from_client, *b"NTSN") + } + const WIRE_LENGTH: usize = 48; const VERSION: u8 = 5; @@ -476,6 +513,7 @@ mod tests { flags: NtpFlags { unknown_leap: i % 3 == 0, interleaved_mode: i % 4 == 0, + status_message: i % 5 == 0, }, root_delay: NtpDuration::from_bits_short([i; 4]), root_dispersion: NtpDuration::from_bits_short([i.wrapping_add(1); 4]), diff --git a/ntp-proto/src/time_types.rs b/ntp-proto/src/time_types.rs index 2207e7512..634b932cc 100644 --- a/ntp-proto/src/time_types.rs +++ b/ntp-proto/src/time_types.rs @@ -552,6 +552,11 @@ impl PollInterval { Self(self.0 + 1).min(limits.max) } + #[must_use] + pub fn force_inc(self) -> Self { + Self(self.0.saturating_add(1)) + } + #[must_use] pub fn dec(self, limits: PollIntervalLimits) -> Self { Self(self.0 - 1).max(limits.min)