diff --git a/CHANGELOG.md b/CHANGELOG.md index 565b1f1156f..4fe0eb2c11e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai - Security/Channels: harden Slack external menu token handling by switching to CSPRNG tokens, validating token shape, requiring user identity for external option lookups, and avoiding fabricated timestamp `trigger_id` fallbacks; also switch Tlon Urbit channel IDs to CSPRNG UUIDs, centralize secure ID/token generation via shared infra helpers, and add a guardrail test to block new runtime `Date.now()+Math.random()` token/id patterns. - Security/Hooks transforms: enforce symlink-safe containment for webhook transform module paths (including `hooks.transformsDir` and `hooks.mappings[].transform.module`) by resolving existing-path ancestors via realpath before import, while preserving in-root symlink support; add regression coverage for both escape and allow cases. This ships in the next npm release. Thanks @aether-ai-agent for reporting. - Telegram/WSL2: disable `autoSelectFamily` by default on WSL2 and memoize WSL2 detection in Telegram network decision logic to avoid repeated sync `/proc/version` probes on fetch/send paths. (#21916) Thanks @MizukiMachine. +- Telegram/Network: default Node 22+ DNS result ordering to `ipv4first` for Telegram fetch paths and add `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER`/`channels.telegram.network.dnsResultOrder` overrides to reduce IPv6-path fetch failures. (#5405) Thanks @Glucksberg. - Telegram/Streaming: preserve archived draft preview mapping after flush and clean superseded reasoning preview bubbles so multi-message preview finals no longer cross-edit or orphan stale messages under send/rotation races. (#23202) Thanks @obviyus. - Telegram/Polling: persist a safe update-offset watermark bounded by pending updates so crash/restart cannot skip queued lower `update_id` updates after out-of-order completion. (#23284) thanks @frankekn. - Slack/Slash commands: preserve the Bolt app receiver when registering external select options handlers so monitor startup does not crash on runtimes that require bound `app.options` calls. (#23209) Thanks @0xgaia. diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index 276938503b0..a9eff8fbdaf 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -58,6 +58,7 @@ export function resolveModel( const authStorage = discoverAuthStorage(resolvedAgentDir); const modelRegistry = discoverModels(authStorage, resolvedAgentDir); const model = modelRegistry.find(provider, modelId) as Model | null; + if (!model) { const providers = cfg?.models?.providers ?? {}; const inlineModels = buildInlineProviderModels(providers); diff --git a/src/config/types.telegram.ts b/src/config/types.telegram.ts index 46438553acf..486bfc104aa 100644 --- a/src/config/types.telegram.ts +++ b/src/config/types.telegram.ts @@ -25,6 +25,12 @@ export type TelegramActionConfig = { export type TelegramNetworkConfig = { /** Override Node's autoSelectFamily behavior (true = enable, false = disable). */ autoSelectFamily?: boolean; + /** + * DNS result order for network requests ("ipv4first" | "verbatim"). + * Set to "ipv4first" to prioritize IPv4 addresses and work around IPv6 issues. + * Default: "ipv4first" on Node 22+ to avoid common fetch failures. + */ + dnsResultOrder?: "ipv4first" | "verbatim"; }; export type TelegramInlineButtonsScope = "off" | "dm" | "group" | "all" | "allowlist"; diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 5fd0ae8fdb3..21bfa047f3d 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -160,6 +160,7 @@ export const TelegramAccountSchemaBase = z network: z .object({ autoSelectFamily: z.boolean().optional(), + dnsResultOrder: z.enum(["ipv4first", "verbatim"]).optional(), }) .strict() .optional(), diff --git a/src/telegram/fetch.test.ts b/src/telegram/fetch.test.ts index 2012fb21777..9f1c676119b 100644 --- a/src/telegram/fetch.test.ts +++ b/src/telegram/fetch.test.ts @@ -3,6 +3,7 @@ import { resolveFetch } from "../infra/fetch.js"; import { resetTelegramFetchStateForTests, resolveTelegramFetch } from "./fetch.js"; const setDefaultAutoSelectFamily = vi.hoisted(() => vi.fn()); +const setDefaultResultOrder = vi.hoisted(() => vi.fn()); vi.mock("node:net", async () => { const actual = await vi.importActual("node:net"); @@ -12,11 +13,20 @@ vi.mock("node:net", async () => { }; }); +vi.mock("node:dns", async () => { + const actual = await vi.importActual("node:dns"); + return { + ...actual, + setDefaultResultOrder, + }; +}); + const originalFetch = globalThis.fetch; afterEach(() => { resetTelegramFetchStateForTests(); - setDefaultAutoSelectFamily.mockClear(); + setDefaultAutoSelectFamily.mockReset(); + setDefaultResultOrder.mockReset(); vi.unstubAllEnvs(); vi.clearAllMocks(); if (originalFetch) { @@ -105,4 +115,22 @@ describe("resolveTelegramFetch", () => { resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } }); expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(false); }); + + it("applies dns result order from config", async () => { + globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; + resolveTelegramFetch(undefined, { network: { dnsResultOrder: "verbatim" } }); + expect(setDefaultResultOrder).toHaveBeenCalledWith("verbatim"); + }); + + it("retries dns setter on next call when previous attempt threw", async () => { + setDefaultResultOrder.mockImplementationOnce(() => { + throw new Error("dns setter failed once"); + }); + globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; + + resolveTelegramFetch(undefined, { network: { dnsResultOrder: "ipv4first" } }); + resolveTelegramFetch(undefined, { network: { dnsResultOrder: "ipv4first" } }); + + expect(setDefaultResultOrder).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/telegram/fetch.ts b/src/telegram/fetch.ts index 05efa5a37df..48fdf72eff7 100644 --- a/src/telegram/fetch.ts +++ b/src/telegram/fetch.ts @@ -1,29 +1,50 @@ +import * as dns from "node:dns"; import * as net from "node:net"; import type { TelegramNetworkConfig } from "../config/types.telegram.js"; import { resolveFetch } from "../infra/fetch.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { resolveTelegramAutoSelectFamilyDecision } from "./network-config.js"; +import { + resolveTelegramAutoSelectFamilyDecision, + resolveTelegramDnsResultOrderDecision, +} from "./network-config.js"; let appliedAutoSelectFamily: boolean | null = null; +let appliedDnsResultOrder: string | null = null; const log = createSubsystemLogger("telegram/network"); // Node 22 workaround: enable autoSelectFamily to allow IPv4 fallback on broken IPv6 networks. // Many networks have IPv6 configured but not routed, causing "Network is unreachable" errors. // See: https://github.com/nodejs/node/issues/54359 function applyTelegramNetworkWorkarounds(network?: TelegramNetworkConfig): void { - const decision = resolveTelegramAutoSelectFamilyDecision({ network }); - if (decision.value === null || decision.value === appliedAutoSelectFamily) { - return; + // Apply autoSelectFamily workaround + const autoSelectDecision = resolveTelegramAutoSelectFamilyDecision({ network }); + if (autoSelectDecision.value !== null && autoSelectDecision.value !== appliedAutoSelectFamily) { + if (typeof net.setDefaultAutoSelectFamily === "function") { + try { + net.setDefaultAutoSelectFamily(autoSelectDecision.value); + appliedAutoSelectFamily = autoSelectDecision.value; + const label = autoSelectDecision.source ? ` (${autoSelectDecision.source})` : ""; + log.info(`autoSelectFamily=${autoSelectDecision.value}${label}`); + } catch { + // ignore if unsupported by the runtime + } + } } - appliedAutoSelectFamily = decision.value; - if (typeof net.setDefaultAutoSelectFamily === "function") { - try { - net.setDefaultAutoSelectFamily(decision.value); - const label = decision.source ? ` (${decision.source})` : ""; - log.debug(`telegram: autoSelectFamily=${decision.value}${label}`); - } catch { - // ignore if unsupported by the runtime + // Apply DNS result order workaround for IPv4/IPv6 issues. + // Some APIs (including Telegram) may fail with IPv6 on certain networks. + // See: https://github.com/openclaw/openclaw/issues/5311 + const dnsDecision = resolveTelegramDnsResultOrderDecision({ network }); + if (dnsDecision.value !== null && dnsDecision.value !== appliedDnsResultOrder) { + if (typeof dns.setDefaultResultOrder === "function") { + try { + dns.setDefaultResultOrder(dnsDecision.value as "ipv4first" | "verbatim"); + appliedDnsResultOrder = dnsDecision.value; + const label = dnsDecision.source ? ` (${dnsDecision.source})` : ""; + log.info(`dnsResultOrder=${dnsDecision.value}${label}`); + } catch { + // ignore if unsupported by the runtime + } } } } @@ -46,4 +67,5 @@ export function resolveTelegramFetch( export function resetTelegramFetchStateForTests(): void { appliedAutoSelectFamily = null; + appliedDnsResultOrder = null; } diff --git a/src/telegram/network-config.test.ts b/src/telegram/network-config.test.ts index 5182f097444..7a7dd197c75 100644 --- a/src/telegram/network-config.test.ts +++ b/src/telegram/network-config.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { resetTelegramNetworkConfigStateForTests, resolveTelegramAutoSelectFamilyDecision, + resolveTelegramDnsResultOrderDecision, } from "./network-config.js"; // Mock isWSL2Sync at the top level @@ -129,3 +130,34 @@ describe("resolveTelegramAutoSelectFamilyDecision", () => { }); }); }); + +describe("resolveTelegramDnsResultOrderDecision", () => { + it("uses env override when provided", () => { + const decision = resolveTelegramDnsResultOrderDecision({ + env: { OPENCLAW_TELEGRAM_DNS_RESULT_ORDER: "verbatim" }, + nodeMajor: 22, + }); + expect(decision).toEqual({ + value: "verbatim", + source: "env:OPENCLAW_TELEGRAM_DNS_RESULT_ORDER", + }); + }); + + it("uses config override when provided", () => { + const decision = resolveTelegramDnsResultOrderDecision({ + network: { dnsResultOrder: "ipv4first" }, + nodeMajor: 20, + }); + expect(decision).toEqual({ value: "ipv4first", source: "config" }); + }); + + it("defaults to ipv4first on Node 22", () => { + const decision = resolveTelegramDnsResultOrderDecision({ nodeMajor: 22 }); + expect(decision).toEqual({ value: "ipv4first", source: "default-node22" }); + }); + + it("returns null when no dns decision applies", () => { + const decision = resolveTelegramDnsResultOrderDecision({ nodeMajor: 20 }); + expect(decision).toEqual({ value: null }); + }); +}); diff --git a/src/telegram/network-config.ts b/src/telegram/network-config.ts index 27815e8d8f4..6bf20567cb7 100644 --- a/src/telegram/network-config.ts +++ b/src/telegram/network-config.ts @@ -6,6 +6,7 @@ import { isWSL2Sync } from "../infra/wsl.js"; export const TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV = "OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY"; export const TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV = "OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY"; +export const TELEGRAM_DNS_RESULT_ORDER_ENV = "OPENCLAW_TELEGRAM_DNS_RESULT_ORDER"; export type TelegramAutoSelectFamilyDecision = { value: boolean | null; @@ -22,6 +23,11 @@ function isWSL2SyncCached(): boolean { return wsl2SyncCache; } +export type TelegramDnsResultOrderDecision = { + value: string | null; + source?: string; +}; + export function resolveTelegramAutoSelectFamilyDecision(params?: { network?: TelegramNetworkConfig; env?: NodeJS.ProcessEnv; @@ -52,6 +58,49 @@ export function resolveTelegramAutoSelectFamilyDecision(params?: { return { value: null }; } +/** + * Resolve DNS result order setting for Telegram network requests. + * Some networks/ISPs have issues with IPv6 causing fetch failures. + * Setting "ipv4first" prioritizes IPv4 addresses in DNS resolution. + * + * Priority: + * 1. Environment variable OPENCLAW_TELEGRAM_DNS_RESULT_ORDER + * 2. Config: channels.telegram.network.dnsResultOrder + * 3. Default: "ipv4first" on Node 22+ (to work around common IPv6 issues) + */ +export function resolveTelegramDnsResultOrderDecision(params?: { + network?: TelegramNetworkConfig; + env?: NodeJS.ProcessEnv; + nodeMajor?: number; +}): TelegramDnsResultOrderDecision { + const env = params?.env ?? process.env; + const nodeMajor = + typeof params?.nodeMajor === "number" + ? params.nodeMajor + : Number(process.versions.node.split(".")[0]); + + // Check environment variable + const envValue = env[TELEGRAM_DNS_RESULT_ORDER_ENV]?.trim().toLowerCase(); + if (envValue === "ipv4first" || envValue === "verbatim") { + return { value: envValue, source: `env:${TELEGRAM_DNS_RESULT_ORDER_ENV}` }; + } + + // Check config + const configValue = (params?.network as { dnsResultOrder?: string } | undefined)?.dnsResultOrder + ?.trim() + .toLowerCase(); + if (configValue === "ipv4first" || configValue === "verbatim") { + return { value: configValue, source: "config" }; + } + + // Default to ipv4first on Node 22+ to avoid IPv6 issues + if (Number.isFinite(nodeMajor) && nodeMajor >= 22) { + return { value: "ipv4first", source: "default-node22" }; + } + + return { value: null }; +} + export function resetTelegramNetworkConfigStateForTests(): void { wsl2SyncCache = undefined; }