From 9706184635ccac3e2c46cac47f0854d5bef218fa Mon Sep 17 00:00:00 2001 From: Chris <102702255+Coldensjo@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:43:28 +0200 Subject: [PATCH] The client dials the first IPv4 address alone, as 1.12.1 does (#1385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem.** `localhost` resolves to `::1` first. benilla dialled every resolved address in order, so a realmd bound only to IPv4 left a Windows login waiting about 2 s on `::1`. **Reference.** 1.12.1 resolves the realmlist host to one IPv4 address and dials it alone (`WowConnection::ConnectToHost` `0x5b6e40`): `inet_addr` (WSOCK32 ordinal 10), else the first entry of `gethostbyname` (ordinal 52). It never dials IPv6. **Change.** The dial takes the resolver's first IPv4 address and nothing else. A host with no IPv4 address fails as unresolved. **Tests.** `localhost` dials `127.0.0.1`. The first IPv4 address of several is dialled. A host that resolves only to IPv6 gives no target. Smoke: green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Sam (cherry picked from commit 8c3587479059423dc69e67a345fc933a9ba40b17) --- crates/benilla-protocol/src/lib.rs | 43 +++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/benilla-protocol/src/lib.rs b/crates/benilla-protocol/src/lib.rs index b10e777c..6fc04d3e 100644 --- a/crates/benilla-protocol/src/lib.rs +++ b/crates/benilla-protocol/src/lib.rs @@ -23,7 +23,7 @@ pub use world::{ WardenRequired, WorldAuthReject, WorldReader, WorldSession, WorldWriter, WORLD_PORT, }; -use std::net::TcpStream; +use std::net::{SocketAddr, TcpStream}; use anyhow::{anyhow, Context, Result}; use benilla_srp::{NormalizedString, PublicKey, SrpClientChallenge, SESSION_KEY_LENGTH}; @@ -81,12 +81,11 @@ impl std::fmt::Display for DialFailure { impl std::error::Error for DialFailure {} -/// Resolves, then tries every resolved address in turn: `localhost` gives `::1` first, and a -/// server may bind only IPv4. +/// Resolves, then dials the first IPv4 address ([`first_ipv4`]). fn dial(host: &str, port: u16) -> Result { use std::net::ToSocketAddrs; let address = format!("{host}:{port}"); - let addrs: Vec = match (host, port).to_socket_addrs() { + let addrs: Vec = match (host, port).to_socket_addrs() { Ok(addrs) => addrs.collect(), Err(e) => { return Err(anyhow::Error::new(DialFailure { @@ -96,13 +95,13 @@ fn dial(host: &str, port: u16) -> Result { .context(format!("resolving {host}: {e}"))) } }; - if addrs.is_empty() { + let Some(target) = first_ipv4(&addrs) else { return Err(anyhow::Error::new(DialFailure { address, unresolved: true, })); - } - TcpStream::connect(&addrs[..]).map_err(|e| { + }; + TcpStream::connect(target).map_err(|e| { anyhow::Error::new(DialFailure { address, unresolved: false, @@ -111,6 +110,13 @@ fn dial(host: &str, port: u16) -> Result { }) } +/// The address 1.12.1 dials: IPv4 alone, the first the resolver gives (`WowConnection::ConnectToHost` +/// `0x5b6e40`: `inet_addr`, else `gethostbyname`'s first entry). `localhost` resolves to `::1` +/// first, which a realmd bound to IPv4 refuses only after about 2 s on Windows. +fn first_ipv4(addrs: &[SocketAddr]) -> Option { + addrs.iter().copied().find(SocketAddr::is_ipv4) +} + /// A realm from realmd's realm list, with every field the wire carries. #[derive(Debug, Clone)] pub struct RealmInfo { @@ -269,3 +275,26 @@ mod host_port_tests { ); } } + +#[cfg(test)] +mod dial_tests { + use super::first_ipv4; + use std::net::SocketAddr; + + fn addrs(list: &[&str]) -> Vec { + list.iter().map(|a| a.parse().unwrap()).collect() + } + + #[test] + fn localhost_dials_its_ipv4_address() { + let got = first_ipv4(&addrs(&["[::1]:3724", "127.0.0.1:3724"])); + assert_eq!(got, Some("127.0.0.1:3724".parse().unwrap())); + } + + #[test] + fn the_first_ipv4_address_is_dialled_and_ipv6_never() { + let got = first_ipv4(&addrs(&["[2001:db8::2]:1", "10.0.0.2:1", "10.0.0.1:1"])); + assert_eq!(got, Some("10.0.0.2:1".parse().unwrap())); + assert_eq!(first_ipv4(&addrs(&["[2001:db8::1]:1"])), None); + } +}