Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
59 changes: 31 additions & 28 deletions mitmproxy-rs/src/udp_client.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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:
Expand Down Expand Up @@ -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<UdpSocket> {
let addrs: Vec<SocketAddr> = lookup_host((host.as_str(), port))
let mut addrs: Vec<SocketAddr> = 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<anyhow::Error> = 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)]
Expand Down
65 changes: 55 additions & 10 deletions src/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,39 @@ impl DnsResolver {
}

fn _interleave_addrinfos(lookup_ip: LookupIp) -> Vec<IpAddr> {
let (mut ipv4_addrs, mut ipv6_addrs): (Vec<IpAddr>, Vec<IpAddr>) =
lookup_ip.into_iter().partition(|addr| addr.is_ipv4());

let mut interleaved: Vec<IpAddr> = Vec::with_capacity(ipv4_addrs.len() + ipv6_addrs.len());
let mut addrs: Vec<IpAddr> = 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<T, F>(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)]
Expand Down Expand Up @@ -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]);
}
}
Loading