diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e3e39a1..171c65dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ ## Unreleased: mitmproxy_rs next +- Fix UDP connection establishment on IPv6-only networks. ## 30 January 2026: mitmproxy_rs 0.12.9 +- Update to Rust Edition 2024. ## 22 November 2025: mitmproxy_rs 0.12.8 +- Fix some bugs related to process icon creation. ## 15 July 2025: mitmproxy_rs 0.12.7 diff --git a/mitmproxy-rs/src/udp_client.rs b/mitmproxy-rs/src/udp_client.rs index 4c791ea4..3a681546 100644 --- a/mitmproxy-rs/src/udp_client.rs +++ b/mitmproxy-rs/src/udp_client.rs @@ -1,7 +1,7 @@ use anyhow::Context; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use anyhow::Result; +use anyhow::{Result, anyhow}; use pyo3::prelude::*; use tokio::net::{UdpSocket, lookup_host}; use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel}; @@ -10,7 +10,6 @@ use tokio::sync::oneshot; use crate::stream::{Stream, StreamState}; use mitmproxy::MAX_PACKET_SIZE; use mitmproxy::messages::{ConnectionId, TransportCommand, TunnelInfo}; - use mitmproxy::packet_sources::udp::remote_host_closed_conn; /// Start a UDP client that is configured with the given parameters: @@ -57,41 +56,45 @@ pub fn open_udp_connection( }) } -/// Open an UDP socket from bind_to to host:port. -/// This is a bit trickier than expected because we want to support IPv4 and IPv6. +/// Open a UDP socket connected to `host:port`. async fn udp_connect( host: String, port: u16, local_addr: Option<(String, u16)>, ) -> Result { - let addrs: Vec = lookup_host((host.as_str(), port)) + let mut addrs: Vec = lookup_host((host.as_str(), port)) .await .with_context(|| format!("unable to resolve hostname: {host}"))? .collect(); + mitmproxy::dns::interleave_inplace(&mut addrs, |a| a.is_ipv4()); + let local_addr = local_addr.as_ref().map(|(h, p)| (h.as_str(), *p)); + + let mut last_err: Option = None; + for addr in addrs { + let socket = match (local_addr, addr) { + (Some((host, port)), _) => UdpSocket::bind((host, port)).await, + (None, SocketAddr::V4(_)) => { + UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)).await + } + (None, SocketAddr::V6(_)) => { + UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)).await + } + }; + let socket = match socket { + Ok(s) => s, + Err(e) => { + last_err = Some(e.into()); + continue; + } + }; + match socket.connect(addr).await { + Ok(()) => return Ok(socket), + Err(e) => last_err = Some(e.into()), + } + } - let socket = if let Some((host, port)) = local_addr { - UdpSocket::bind((host.as_str(), port)) - .await - .with_context(|| format!("unable to bind to ({host}, {port})"))? - } else if addrs.iter().any(|x| x.is_ipv4()) { - // we initially tried to bind to IPv6 by default if that doesn't fail, - // but binding mysteriously works if there are only IPv4 addresses in addrs, - // and then we get a weird "invalid argument" error when calling socket.recv(). - // So we just do the lazy thing and do IPv4 by default. - UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)) - .await - .context("unable to bind to 127.0.0.1:0")? - } else { - UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)) - .await - .context("unable to bind to [::]:0")? - }; - - socket - .connect(addrs.as_slice()) - .await - .with_context(|| format!("unable to connect to {host}"))?; - Ok(socket) + Err(last_err.unwrap_or_else(|| anyhow!("unable to resolve hostname: no addresses for {host}"))) + .with_context(|| format!("unable to connect to {host}")) } #[derive(Debug)] diff --git a/src/dns.rs b/src/dns.rs index b000b51c..074181b2 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -97,19 +97,39 @@ impl DnsResolver { } fn _interleave_addrinfos(lookup_ip: LookupIp) -> Vec { - let (mut ipv4_addrs, mut ipv6_addrs): (Vec, Vec) = - lookup_ip.into_iter().partition(|addr| addr.is_ipv4()); - - let mut interleaved: Vec = Vec::with_capacity(ipv4_addrs.len() + ipv6_addrs.len()); + let mut addrs: Vec = lookup_ip.into_iter().collect(); + interleave_inplace(&mut addrs, |a| a.is_ipv4()); + addrs +} - while let Some(ipv4) = ipv4_addrs.pop() { - interleaved.push(ipv4); - if let Some(ipv6) = ipv6_addrs.pop() { - interleaved.push(ipv6); +/// Reorder `items` in place so that elements matching `predicate` are interleaved +/// with non-matching ones, starting with a matching element. Leftover elements of +/// either kind are appended at the end. O(n) swaps. +pub fn interleave_inplace(items: &mut [T], mut predicate: F) +where + F: FnMut(&T) -> bool, +{ + let mut lookahead = 1; + let mut expects = true; + let mut i = 0; + while i < items.len() { + if predicate(&items[i]) != expects { + let Some(off) = items[lookahead..] + .iter() + .position(|x| predicate(x) == expects) + else { + break; + }; + lookahead += off; + items.swap(i, lookahead); + lookahead += 1; + i += 2; + } else { + i += 1; + expects = !expects; + lookahead = i + 1; } } - interleaved.append(&mut ipv6_addrs); - interleaved } #[cfg(test)] @@ -196,4 +216,29 @@ mod tests { tokio::spawn(async move { server.block_until_done().await }); Ok(listen_addr) } + + #[test] + fn interleave_more_matches_than_misses() { + let mut items = vec![false, true, false, true, true]; + interleave_inplace(&mut items, |b| *b); + assert_eq!(items, vec![true, false, true, false, true]); + } + + #[test] + fn interleave_more_misses_than_matches() { + let mut items = vec![false, false, false, true]; + interleave_inplace(&mut items, |b| *b); + assert_eq!(items, vec![true, false, false, false]); + } + + #[test] + fn interleave_single_kind() { + let mut items = vec![true, true, true]; + interleave_inplace(&mut items, |b| *b); + assert_eq!(items, vec![true, true, true]); + + let mut items = vec![false, false]; + interleave_inplace(&mut items, |b| *b); + assert_eq!(items, vec![false, false]); + } }