{"repo":"NousResearch/hermes-agent","public_id":"14520","display_label":"DoH Telegram IP filtering edge case fix","summary":"These issues and PRs address a bug where Telegram polling fails when DNS-over-HTTPS resolves to the same IP address as the system DNS. The fixes aim to preserve DoH-confirmed IPs in this matching scenario rather than discarding them.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":20175,"merged_at":"2026-05-05T11:43:00+00:00"},"resolution":{"state":"resolved","by":"pr-20175"},"freshness":{"state":"verified","last_verified_at":"2026-05-05T11:43:00+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":14520,"pr_number":20175,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-05T11:43:00+00:00"}],"members":[{"kind":"issue","number":14520,"title":"Telegram polling fails when DoH returns same IP as system DNS","url":null,"state":"closed"},{"kind":"pr","number":17043,"title":"fix(gateway): keep DoH-confirmed Telegram IPs that match system DNS (#14520)","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":20175,"title":"fix(gateway): keep DoH-confirmed Telegram IPs that match system DNS (salvage #17043)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/telegram_network.py","tests/gateway/test_telegram_network.py"],"fix_hunks":[{"file":"gateway/platforms/telegram_network.py","hunks":["@@ -185,10 +185,13 @@ async def _query_doh_provider(\n async def discover_fallback_ips() -> list[str]:\n     \"\"\"Auto-discover Telegram API IPs via DNS-over-HTTPS.\n \n-    Resolves api.telegram.org through Google and Cloudflare DoH, collects all\n-    unique IPs, and excludes the system-DNS-resolved IP (which is presumably\n-    unreachable on this network).  Falls back to a hardcoded seed list when DoH\n-    is also unavailable.\n+    Resolves api.telegram.org through Google and Cloudflare DoH and returns all\n+    unique A records.  IPs that match the local system resolver are kept rather\n+    than excluded: in many networks the system-DNS IP is the most reliable path\n+    to api.telegram.org and a transient primary-path failure should be retried\n+    against the same address via the IP-rewrite path before the seed list is\n+    consulted (#14520).  Falls back to a hardcoded seed list only when DoH\n+    yields no usable answers.\n     \"\"\"\n     async with httpx.AsyncClient(timeout=httpx.Timeout(_DOH_TIMEOUT)) as client:\n         doh_tasks = [_query_doh_provider(client, p) for p in _DOH_PROVIDERS]","@@ -203,11 +206,11 @@ async def discover_fallback_ips() -> list[str]:\n         if isinstance(r, list):\n             doh_ips.extend(r)\n \n-    # Deduplicate preserving order, exclude system-DNS IPs\n+    # Deduplicate preserving order\n     seen: set[str] = set()\n     candidates: list[str] = []\n     for ip in doh_ips:\n-        if ip not in seen and ip not in system_ips:\n+        if ip not in seen:\n             seen.add(ip)\n             candidates.append(ip)\n ","@@ -219,7 +222,7 @@ async def discover_fallback_ips() -> list[str]:\n         return validated\n \n     logger.info(\n-        \"DoH discovery yielded no new IPs (system DNS: %s); using seed fallback IPs %s\",\n+        \"DoH discovery yielded no usable IPs (system DNS: %s); using seed fallback IPs %s\",\n         \", \".join(system_ips) or \"unknown\",\n         \", \".join(_SEED_FALLBACK_IPS),\n     )"]},{"file":"tests/gateway/test_telegram_network.py","hunks":["@@ -534,15 +534,20 @@ async def test_google_and_cloudflare_ips_collected(self, monkeypatch):\n         assert \"149.154.167.221\" in ips\n \n     @pytest.mark.asyncio\n-    async def test_system_dns_ip_excluded(self, monkeypatch):\n-        \"\"\"The IP from system DNS is the one that doesn't work — exclude it.\"\"\"\n+    async def test_system_dns_ip_kept_when_doh_confirms(self, monkeypatch):\n+        \"\"\"DoH-confirmed IPs are kept even when they match system DNS (#14520).\n+\n+        The system-DNS IP is often the most reliable path; including it as a\n+        fallback lets the IP-rewrite retry recover from transient primary-path\n+        failures instead of jumping straight to the hardcoded seed list.\n+        \"\"\"\n         self._patch_doh(monkeypatch, {\n             \"https://dns.google\": (200, _doh_answer(\"149.154.166.110\", \"149.154.167.220\")),\n             \"https://cloudflare-dns.com\": (200, _doh_answer(\"149.154.166.110\")),\n         }, system_dns_ips=[\"149.154.166.110\"])\n \n         ips = await tnet.discover_fallback_ips()\n-        assert ips == [\"149.154.167.220\"]\n+        assert ips == [\"149.154.166.110\", \"149.154.167.220\"]\n \n     @pytest.mark.asyncio\n     async def test_doh_results_deduplicated(self, monkeypatch):","@@ -607,15 +612,21 @@ async def test_system_dns_failure_keeps_all_doh_ips(self, monkeypatch):\n         assert \"149.154.167.220\" in ips\n \n     @pytest.mark.asyncio\n-    async def test_all_doh_ips_same_as_system_dns_uses_seed(self, monkeypatch):\n-        \"\"\"DoH returns only the same blocked IP — seed list is the fallback.\"\"\"\n+    async def test_all_doh_ips_same_as_system_dns_kept(self, monkeypatch):\n+        \"\"\"DoH agrees with system DNS — keep that IP instead of seed list (#14520).\n+\n+        Previous behavior fell through to ``_SEED_FALLBACK_IPS`` here, but the\n+        seed addresses are not routable on every network.  When DoH confirms\n+        the system IP, that IP is the best candidate we have and should be\n+        used as the fallback target.\n+        \"\"\"\n         self._patch_doh(monkeypatch, {\n             \"https://dns.google\": (200, _doh_answer(\"149.154.166.110\")),\n             \"https://cloudflare-dns.com\": (200, _doh_answer(\"149.154.166.110\")),\n         }, system_dns_ips=[\"149.154.166.110\"])\n \n         ips = await tnet.discover_fallback_ips()\n-        assert ips == tnet._SEED_FALLBACK_IPS\n+        assert ips == [\"149.154.166.110\"]\n \n     @pytest.mark.asyncio\n     async def test_cloudflare_gets_accept_header(self, monkeypatch):\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}