fix: sort IPv4 addresses before IPv6 in SSRF pinned DNS to fix Telegram media fetch on IPv6-broken hosts

On hosts where IPv6 is configured but not routed (common on cloud VMs),
Telegram media downloads fail because the pinned DNS lookup may return
IPv6 addresses first. Even though autoSelectFamily (Happy Eyeballs) is
enabled, the round-robin pinned lookup serves individual IPv6 addresses
that fail before IPv4 is attempted.

Sort resolved addresses so IPv4 comes first, ensuring both Happy Eyeballs
and single-address round-robin try the working address family first.

Fixes #23975

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Glucksberg
2026-02-23 02:21:34 +00:00
committed by Peter Steinberger
parent fb8edebc32
commit dd9ba974d0
2 changed files with 29 additions and 1 deletions

View File

@@ -155,6 +155,23 @@ describe("ssrf pinning", () => {
expect(lookup).not.toHaveBeenCalled();
});
it("sorts IPv4 addresses before IPv6 in pinned results", async () => {
const lookup = vi.fn(async () => [
{ address: "2001:db8::1", family: 6 },
{ address: "93.184.216.34", family: 4 },
{ address: "2001:db8::2", family: 6 },
{ address: "93.184.216.35", family: 4 },
]) as unknown as LookupFn;
const pinned = await resolvePinnedHostname("example.com", lookup);
expect(pinned.addresses).toEqual([
"93.184.216.34",
"93.184.216.35",
"2001:db8::1",
"2001:db8::2",
]);
});
it("allows ISATAP embedded private IPv4 when private network is explicitly enabled", async () => {
const lookup = vi.fn(async () => [
{ address: "2001:db8:1234::5efe:127.0.0.1", family: 6 },

View File

@@ -290,7 +290,18 @@ export async function resolvePinnedHostnameWithPolicy(
assertAllowedResolvedAddressesOrThrow(results, params.policy);
}
const addresses = Array.from(new Set(results.map((entry) => entry.address)));
// Sort IPv4 addresses before IPv6 so that Happy Eyeballs (autoSelectFamily) and
// round-robin pinned lookups try IPv4 first. This avoids connection failures on
// hosts where IPv6 is configured but not routed (common on cloud VMs and WSL2).
// See: https://github.com/openclaw/openclaw/issues/23975
const addresses = Array.from(new Set(results.map((entry) => entry.address))).toSorted((a, b) => {
const aIsV6 = a.includes(":");
const bIsV6 = b.includes(":");
if (aIsV6 === bIsV6) {
return 0;
}
return aIsV6 ? 1 : -1;
});
if (addresses.length === 0) {
throw new Error(`Unable to resolve hostname: ${hostname}`);
}