The client dials the first IPv4 address alone, as 1.12.1 does (#1385)

**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 <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
(cherry picked from commit 8c3587479059423dc69e67a345fc933a9ba40b17)
This commit is contained in:
Chris
2026-10-25 23:00:49 -06:00
committed by grom
parent b42cd7ef6b
commit 9706184635
+36 -7
View File
@@ -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<TcpStream> {
use std::net::ToSocketAddrs;
let address = format!("{host}:{port}");
let addrs: Vec<std::net::SocketAddr> = match (host, port).to_socket_addrs() {
let addrs: Vec<SocketAddr> = 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<TcpStream> {
.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<TcpStream> {
})
}
/// 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<SocketAddr> {
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<SocketAddr> {
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);
}
}