diff --git a/src/config.rs b/src/config.rs index 7bb85940b..3ae9457a8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -585,20 +585,11 @@ impl TrippyConfig { (true, _, _, _) => IpAddrFamily::Ipv4Only, (_, true, _, _) => IpAddrFamily::Ipv6Only, }; - - #[allow(clippy::match_same_arms)] - let multipath_strategy = match (multipath_strategy_cfg, addr_family) { - (MultipathStrategyConfig::Classic, _) => Ok(MultipathStrategy::Classic), - (MultipathStrategyConfig::Paris, _) => Ok(MultipathStrategy::Paris), - ( - MultipathStrategyConfig::Dublin, - IpAddrFamily::Ipv4Only | IpAddrFamily::Ipv4thenIpv6 | IpAddrFamily::Ipv6thenIpv4, - ) => Ok(MultipathStrategy::Dublin), - (MultipathStrategyConfig::Dublin, IpAddrFamily::Ipv6Only) => Err(anyhow!( - "Dublin multipath strategy not implemented for IPv6 yet!" - )), - }?; - + let multipath_strategy = match multipath_strategy_cfg { + MultipathStrategyConfig::Classic => MultipathStrategy::Classic, + MultipathStrategyConfig::Paris => MultipathStrategy::Paris, + MultipathStrategyConfig::Dublin => MultipathStrategy::Dublin, + }; let port_direction = match (protocol, source_port, target_port, multipath_strategy_cfg) { (Protocol::Icmp, _, _, _) => PortDirection::None, (Protocol::Udp, None, None, _) => PortDirection::new_fixed_src(pid.max(1024)), diff --git a/src/main.rs b/src/main.rs index 34cf358cf..16ca2bf65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -295,6 +295,7 @@ fn make_channel_config( target_addr, args.packet_size, args.payload_pattern, + args.initial_sequence, args.tos, args.icmp_extension_parse_mode, args.read_timeout, diff --git a/src/tracing/config.rs b/src/tracing/config.rs index e11032d47..d559b7946 100644 --- a/src/tracing/config.rs +++ b/src/tracing/config.rs @@ -360,6 +360,7 @@ pub struct ChannelConfig { pub target_addr: IpAddr, pub packet_size: PacketSize, pub payload_pattern: PayloadPattern, + pub initial_sequence: Sequence, pub tos: TypeOfService, pub icmp_extension_mode: IcmpExtensionParseMode, pub read_timeout: Duration, @@ -376,6 +377,7 @@ impl ChannelConfig { target_addr: IpAddr, packet_size: u16, payload_pattern: u8, + initial_sequence: u16, tos: u8, icmp_extension_mode: IcmpExtensionParseMode, read_timeout: Duration, @@ -388,6 +390,7 @@ impl ChannelConfig { target_addr, packet_size: PacketSize(packet_size), payload_pattern: PayloadPattern(payload_pattern), + initial_sequence: Sequence(initial_sequence), tos: TypeOfService(tos), icmp_extension_mode, read_timeout, @@ -405,6 +408,7 @@ impl Default for ChannelConfig { target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), packet_size: PacketSize(defaults::DEFAULT_STRATEGY_PACKET_SIZE), payload_pattern: PayloadPattern(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN), + initial_sequence: Sequence(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE), tos: TypeOfService(defaults::DEFAULT_STRATEGY_TOS), icmp_extension_mode: defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE, read_timeout: defaults::DEFAULT_STRATEGY_READ_TIMEOUT, diff --git a/src/tracing/net/channel.rs b/src/tracing/net/channel.rs index 7eeb6b064..a08e49af2 100644 --- a/src/tracing/net/channel.rs +++ b/src/tracing/net/channel.rs @@ -4,7 +4,7 @@ use crate::tracing::net::socket::Socket; use crate::tracing::net::{ipv4, ipv6, platform, Network}; use crate::tracing::probe::{Probe, ProbeResponse}; use crate::tracing::types::{PacketSize, PayloadPattern, TypeOfService}; -use crate::tracing::{ChannelConfig, Port, PrivilegeMode, Protocol}; +use crate::tracing::{ChannelConfig, Port, PrivilegeMode, Protocol, Sequence}; use arrayvec::ArrayVec; use std::net::IpAddr; use std::time::{Duration, SystemTime}; @@ -25,6 +25,7 @@ pub struct TracerChannel { dest_addr: IpAddr, packet_size: PacketSize, payload_pattern: PayloadPattern, + initial_sequence: Sequence, tos: TypeOfService, icmp_extension_mode: IcmpExtensionParseMode, read_timeout: Duration, @@ -63,6 +64,7 @@ impl TracerChannel { dest_addr: config.target_addr, packet_size: config.packet_size, payload_pattern: config.payload_pattern, + initial_sequence: config.initial_sequence, tos: config.tos, icmp_extension_mode: config.icmp_extension_mode, read_timeout: config.read_timeout, @@ -154,6 +156,7 @@ impl TracerChannel { self.privilege_mode, self.packet_size, self.payload_pattern, + self.initial_sequence, ) } _ => unreachable!(), diff --git a/src/tracing/net/ipv4.rs b/src/tracing/net/ipv4.rs index 58a836019..26dd197bc 100644 --- a/src/tracing/net/ipv4.rs +++ b/src/tracing/net/ipv4.rs @@ -431,13 +431,14 @@ fn extract_probe_resp_seq( } (Protocol::Udp, IpProtocol::Udp) => { let (src_port, dest_port, checksum, identifier) = extract_udp_packet(ipv4)?; - Some(ProbeResponseSeq::Udp(ProbeResponseSeqUdp::new( identifier, IpAddr::V4(ipv4.get_destination()), src_port, dest_port, checksum, + 0, + false, ))) } (Protocol::Tcp, IpProtocol::Tcp) => { @@ -1137,6 +1138,8 @@ mod tests { src_port, dest_port, checksum, + payload_len, + has_magic, }), .. }, @@ -1154,6 +1157,8 @@ mod tests { assert_eq!(31829, src_port); assert_eq!(33030, dest_port); assert_eq!(58571, checksum); + assert_eq!(0, payload_len); + assert!(!has_magic); assert_eq!(None, extensions); Ok(()) } @@ -1189,6 +1194,8 @@ mod tests { src_port, dest_port, checksum, + payload_len, + has_magic, }), .. }, @@ -1206,6 +1213,8 @@ mod tests { assert_eq!(32779, src_port); assert_eq!(33010, dest_port); assert_eq!(10913, checksum); + assert_eq!(0, payload_len); + assert!(!has_magic); assert_eq!(None, extensions); Ok(()) } diff --git a/src/tracing/net/ipv6.rs b/src/tracing/net/ipv6.rs index c9dd23dbd..57534e3cf 100644 --- a/src/tracing/net/ipv6.rs +++ b/src/tracing/net/ipv6.rs @@ -44,6 +44,9 @@ const MIN_PACKET_SIZE_ICMP: usize = const MIN_PACKET_SIZE_UDP: usize = Ipv6Packet::minimum_packet_size() + UdpPacket::minimum_packet_size(); +/// Magic prefix for IPv6/UDP/Dublin payloads. +const MAGIC: &[u8] = b"trippy"; + #[instrument(skip(icmp_send_socket, probe))] pub fn dispatch_icmp_probe( icmp_send_socket: &mut S, @@ -73,6 +76,7 @@ pub fn dispatch_icmp_probe( Ok(()) } +#[allow(clippy::too_many_arguments)] #[instrument(skip(raw_send_socket, probe))] pub fn dispatch_udp_probe( raw_send_socket: &mut S, @@ -82,6 +86,7 @@ pub fn dispatch_udp_probe( privilege_mode: PrivilegeMode, packet_size: PacketSize, payload_pattern: PayloadPattern, + initial_sequence: Sequence, ) -> TraceResult<()> { let packet_size = usize::from(packet_size.0); if !(MIN_PACKET_SIZE_UDP..=MAX_PACKET_SIZE).contains(&packet_size) { @@ -90,9 +95,15 @@ pub fn dispatch_udp_probe( let payload_size = udp_payload_size(packet_size); let payload = &[payload_pattern.0; MAX_UDP_PAYLOAD_BUF][0..payload_size]; match privilege_mode { - PrivilegeMode::Privileged => { - dispatch_udp_probe_raw(raw_send_socket, probe, src_addr, dest_addr, payload) - } + PrivilegeMode::Privileged => dispatch_udp_probe_raw( + raw_send_socket, + probe, + src_addr, + dest_addr, + payload, + payload_pattern, + initial_sequence, + ), PrivilegeMode::Unprivileged => { dispatch_udp_probe_non_raw::(probe, src_addr, dest_addr, payload) } @@ -106,11 +117,18 @@ fn dispatch_udp_probe_raw( src_addr: Ipv6Addr, dest_addr: Ipv6Addr, payload: &[u8], + payload_pattern: PayloadPattern, + initial_sequence: Sequence, ) -> TraceResult<()> { let mut udp_buf = [0_u8; MAX_UDP_PACKET_BUF]; + let mut dublin_payload = [payload_pattern.0; MAX_UDP_PAYLOAD_BUF]; let payload_paris = probe.sequence.0.to_be_bytes(); let payload = if probe.flags.contains(Flags::PARIS_CHECKSUM) { payload_paris.as_slice() + } else if probe.flags.contains(Flags::DUBLIN_IPV6_PAYLOAD_LENGTH) { + let payload_len = probe.sequence.0 - initial_sequence.0; + dublin_payload[..MAGIC.len()].copy_from_slice(MAGIC); + &dublin_payload[..usize::from(payload_len) + MAGIC.len()] } else { payload }; @@ -371,13 +389,21 @@ fn extract_probe_resp_seq( ))) } (Protocol::Udp, IpProtocol::Udp) => { - let (src_port, dest_port, checksum) = extract_udp_packet(ipv6)?; + let (src_port, dest_port, checksum, udp_payload_len) = extract_udp_packet(ipv6)?; + let has_magic = udp_payload_has_magic_prefix(ipv6)?; + let payload_len = if has_magic { + udp_payload_len - MAGIC.len() as u16 + } else { + udp_payload_len + }; Some(ProbeResponseSeq::Udp(ProbeResponseSeqUdp::new( 0, IpAddr::V6(ipv6.get_destination_address()), src_port, dest_port, checksum, + payload_len, + has_magic, ))) } (Protocol::Tcp, IpProtocol::Tcp) => { @@ -400,12 +426,13 @@ fn extract_echo_request(ipv6: &Ipv6Packet<'_>) -> TraceResult<(u16, u16)> { )) } -fn extract_udp_packet(ipv6: &Ipv6Packet<'_>) -> TraceResult<(u16, u16, u16)> { +fn extract_udp_packet(ipv6: &Ipv6Packet<'_>) -> TraceResult<(u16, u16, u16, u16)> { let udp_packet = UdpPacket::new_view(ipv6.payload())?; Ok(( udp_packet.get_source(), udp_packet.get_destination(), udp_packet.get_checksum(), + udp_packet.get_length() - UdpPacket::minimum_packet_size() as u16, )) } @@ -433,6 +460,11 @@ fn extract_tcp_packet(ipv6: &Ipv6Packet<'_>) -> TraceResult<(u16, u16)> { Ok((tcp_packet.get_source(), tcp_packet.get_destination())) } +fn udp_payload_has_magic_prefix(ipv6: &Ipv6Packet<'_>) -> TraceResult { + let udp_packet = UdpPacket::new_view(ipv6.payload())?; + Ok(udp_packet.payload().starts_with(MAGIC)) +} + #[cfg(test)] mod tests { use super::*; @@ -574,6 +606,7 @@ mod tests { let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(48); let payload_pattern = PayloadPattern(0x00); + let initial_sequence = Sequence(33000); let expected_send_to_buf = hex_literal::hex!("00 7b 01 c8 00 08 7a ed"); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0); @@ -600,6 +633,7 @@ mod tests { privilege_mode, packet_size, payload_pattern, + initial_sequence, )?; Ok(()) } @@ -612,6 +646,7 @@ mod tests { let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(56); let payload_pattern = PayloadPattern(0xaa); + let initial_sequence = Sequence(33000); let expected_send_to_buf = hex_literal::hex!( " 00 7b 01 c8 00 10 d0 32 aa aa aa aa aa aa aa aa @@ -642,6 +677,7 @@ mod tests { privilege_mode, packet_size, payload_pattern, + initial_sequence, )?; Ok(()) } @@ -659,6 +695,7 @@ mod tests { // fixed two byte payload is used to hold the sequence let packet_size = PacketSize(300); let payload_pattern = PayloadPattern(0xaa); + let initial_sequence = Sequence(33000); let expected_send_to_buf = hex_literal::hex!( " 00 7b 01 c8 00 0a 80 e8 fa 00 @@ -689,6 +726,7 @@ mod tests { privilege_mode, packet_size, payload_pattern, + initial_sequence, )?; Ok(()) } @@ -702,6 +740,7 @@ mod tests { let privilege_mode = PrivilegeMode::Unprivileged; let packet_size = PacketSize(48); let payload_pattern = PayloadPattern(0x00); + let initial_sequence = Sequence(33000); let expected_send_to_buf = hex_literal::hex!(""); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456); let expected_bind_addr = SocketAddr::new(IpAddr::V6(src_addr), 123); @@ -744,6 +783,7 @@ mod tests { privilege_mode, packet_size, payload_pattern, + initial_sequence, )?; Ok(()) } @@ -757,6 +797,7 @@ mod tests { let privilege_mode = PrivilegeMode::Unprivileged; let packet_size = PacketSize(56); let payload_pattern = PayloadPattern(0x1f); + let initial_sequence = Sequence(33000); let expected_send_to_buf = hex_literal::hex!("1f 1f 1f 1f 1f 1f 1f 1f"); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456); let expected_bind_addr = SocketAddr::new(IpAddr::V6(src_addr), 123); @@ -799,6 +840,7 @@ mod tests { privilege_mode, packet_size, payload_pattern, + initial_sequence, )?; Ok(()) } @@ -811,6 +853,7 @@ mod tests { let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(47); let payload_pattern = PayloadPattern(0x00); + let initial_sequence = Sequence(33000); let mut mocket = MockSocket::new(); let err = dispatch_udp_probe( &mut mocket, @@ -820,6 +863,7 @@ mod tests { privilege_mode, packet_size, payload_pattern, + initial_sequence, ) .unwrap_err(); assert!(matches!(err, TracerError::InvalidPacketSize(_))); @@ -834,6 +878,7 @@ mod tests { let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(1025); let payload_pattern = PayloadPattern(0x00); + let initial_sequence = Sequence(33000); let mut mocket = MockSocket::new(); let err = dispatch_udp_probe( &mut mocket, @@ -843,6 +888,7 @@ mod tests { privilege_mode, packet_size, payload_pattern, + initial_sequence, ) .unwrap_err(); assert!(matches!(err, TracerError::InvalidPacketSize(_))); @@ -1068,6 +1114,8 @@ mod tests { src_port, dest_port, checksum, + payload_len, + has_magic, }), .. }, @@ -1085,6 +1133,8 @@ mod tests { assert_eq!(22694, src_port); assert_eq!(33029, dest_port); assert_eq!(53489, checksum); + assert_eq!(36, payload_len); + assert!(!has_magic); assert_eq!(None, extensions); Ok(()) } @@ -1124,6 +1174,8 @@ mod tests { src_port, dest_port, checksum, + payload_len, + has_magic, }), .. }, @@ -1141,6 +1193,74 @@ mod tests { assert_eq!(26477, src_port); assert_eq!(33118, dest_port); assert_eq!(37906, checksum); + assert_eq!(36, payload_len); + assert!(!has_magic); + assert_eq!(None, extensions); + Ok(()) + } + + // Here we receive a TimeExceeded in UDP/Dublin mode and so extract the + // sequence from the length of the DP payload, after subtracting the length + // of the magic prefix "trippy" (11 - 6 == 5). + // + // Note we do not know if we are in UDP/Dublin mode when decoding the + // packet and so the decision to reject the probe response is left to + // the `tracer::Tracer::validate(..)` function. + #[test] + fn test_recv_icmp_probe_time_exceeded_udp_dublin_with_magic() -> anyhow::Result<()> { + let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c").unwrap()); + let expected_recv_from_buf = hex_literal::hex!( + " + 03 00 23 6f 00 00 00 00 60 0e 0e 00 00 13 11 01 + fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a + 2a 00 14 50 40 09 08 20 00 00 00 00 00 00 20 0e + 80 e8 13 88 00 13 9a 42 74 72 69 70 70 79 00 00 + 00 00 00 + " + ); + let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); + let mut mocket = MockSocket::new(); + mocket + .expect_recv_from() + .times(1) + .returning(mocket_recv_from!( + expected_recv_from_buf, + expected_recv_from_addr + )); + let resp = + recv_icmp_probe(&mut mocket, Protocol::Udp, IcmpExtensionParseMode::Disabled)?.unwrap(); + + let ProbeResponse::TimeExceeded( + ProbeResponseData { + addr, + resp_seq: + ProbeResponseSeq::Udp(ProbeResponseSeqUdp { + identifier, + dest_addr, + src_port, + dest_port, + checksum, + payload_len, + has_magic, + }), + .. + }, + extensions, + ) = resp + else { + panic!("expected TimeExceeded") + }; + assert_eq!(recv_from_addr, addr); + assert_eq!(0, identifier); + assert_eq!( + IpAddr::V6(Ipv6Addr::from_str("2a00:1450:4009:820::200e").unwrap()), + dest_addr + ); + assert_eq!(33000, src_port); + assert_eq!(5000, dest_port); + assert_eq!(39490, checksum); + assert_eq!(5, payload_len); + assert!(has_magic); assert_eq!(None, extensions); Ok(()) } diff --git a/src/tracing/probe.rs b/src/tracing/probe.rs index f9d659f7b..c5bb2459a 100644 --- a/src/tracing/probe.rs +++ b/src/tracing/probe.rs @@ -231,6 +231,8 @@ pub struct ProbeResponseSeqUdp { pub src_port: u16, pub dest_port: u16, pub checksum: u16, + pub payload_len: u16, + pub has_magic: bool, } impl ProbeResponseSeqUdp { @@ -240,6 +242,8 @@ impl ProbeResponseSeqUdp { src_port: u16, dest_port: u16, checksum: u16, + payload_len: u16, + has_magic: bool, ) -> Self { Self { identifier, @@ -247,6 +251,8 @@ impl ProbeResponseSeqUdp { src_port, dest_port, checksum, + payload_len, + has_magic, } } } diff --git a/src/tracing/tracer.rs b/src/tracing/tracer.rs index 658413bfc..a87901fbc 100644 --- a/src/tracing/tracer.rs +++ b/src/tracing/tracer.rs @@ -267,9 +267,18 @@ impl)> Tracer { dest_addr, src_port, dest_port, + has_magic, .. - }) - | ProbeResponseSeq::Tcp(ProbeResponseSeqTcp { + }) => { + let check_ports = validate_ports(self.config.port_direction, src_port, dest_port); + let check_dest_addr = self.config.target_addr == dest_addr; + let check_magic = match (self.config.multipath_strategy, self.config.target_addr) { + (MultipathStrategy::Dublin, IpAddr::V6(_)) => has_magic, + _ => true, + }; + check_dest_addr && check_ports && check_magic + } + ProbeResponseSeq::Tcp(ProbeResponseSeqTcp { dest_addr, src_port, dest_port, @@ -300,13 +309,21 @@ impl)> Tracer { src_port, dest_port, checksum, + payload_len, .. }) => { - let sequence = match (self.config.multipath_strategy, self.config.port_direction) { - (MultipathStrategy::Classic, PortDirection::FixedDest(_)) => src_port, - (MultipathStrategy::Classic, _) => dest_port, - (MultipathStrategy::Paris, _) => checksum, - (MultipathStrategy::Dublin, _) => identifier, + let sequence = match ( + self.config.multipath_strategy, + self.config.port_direction, + self.config.target_addr, + ) { + (MultipathStrategy::Classic, PortDirection::FixedDest(_), _) => src_port, + (MultipathStrategy::Classic, _, _) => dest_port, + (MultipathStrategy::Paris, _, _) => checksum, + (MultipathStrategy::Dublin, _, IpAddr::V4(_)) => identifier, + (MultipathStrategy::Dublin, _, IpAddr::V6(_)) => { + self.config.initial_sequence.0 + payload_len + } }; (TraceId(0), Sequence(sequence), resp.recv, resp.addr) } @@ -663,19 +680,19 @@ mod state { Port(src_port.0), Port(round_port), TraceId(self.sequence.0), - Flags::empty(), + Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, ), PortDirection::FixedDest(dest_port) => ( Port(round_port), Port(dest_port.0), TraceId(self.sequence.0), - Flags::empty(), + Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, ), PortDirection::FixedBoth(src_port, dest_port) => ( Port(src_port.0), Port(dest_port.0), TraceId(self.sequence.0), - Flags::empty(), + Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, ), PortDirection::None => unimplemented!(), } @@ -850,7 +867,7 @@ mod state { /// wrapping during a round, which is more problematic. #[instrument(skip(self))] pub fn advance_round(&mut self, first_ttl: TimeToLive) { - if self.sequence >= MAX_SEQUENCE { + if self.sequence >= self.max_sequence() { self.sequence = self.config.initial_sequence; } self.target_found = false; @@ -861,6 +878,25 @@ mod state { self.round += Round(1); self.ttl = first_ttl; } + + /// The maximum sequence number allowed. + /// + /// The Dublin multipath strategy for IPv6/udp encodes the sequence + /// number as the payload length and consequently the maximum sequence + /// number must be no larger than the maximum IPv6/udp payload size. + /// + /// It is also required that the range of possible sequence numbers is + /// _at least_ `BUFFER_SIZE` to ensure delayed responses from a prior + /// round are not incorrectly associated with later rounds (see + /// `in_round` function). + fn max_sequence(&self) -> Sequence { + match (self.config.multipath_strategy, self.config.target_addr) { + (MultipathStrategy::Dublin, IpAddr::V6(_)) => { + self.config.initial_sequence + Sequence(BUFFER_SIZE) + } + _ => MAX_SEQUENCE, + } + } } #[cfg(test)] diff --git a/src/tracing/types.rs b/src/tracing/types.rs index 39f38a4eb..ab366a5fe 100644 --- a/src/tracing/types.rs +++ b/src/tracing/types.rs @@ -48,6 +48,8 @@ bitflags! { pub struct Flags: u32 { /// Swap the checksum and payload (UDP only). const PARIS_CHECKSUM = 1; + /// Encode the sequence number as the payload length (IPv6/UDP only) + const DUBLIN_IPV6_PAYLOAD_LENGTH = 2; } }