Skip to content

[Bug] Model discovery blocked by destination policy under Clash/Mihomo IPv6 fake-ip (fdfe:dcba:9876::/48) #3462

Closed

Description

@tanrenjun

Client or integration

OpenCodex dashboard

Area

Proxy and routing

Summary

When running OpenCodex behind Clash Verge / Mihomo (Clash.Meta) with Fake-IP mode enabled on a dual-stack IPv6 network, domain resolution for remote providers (e.g. opencode.ai) returns Mihomo's standard IPv6 Fake-IP address range (fake-ip-range6: fdfe:dcba:9876::1/64). Outbound management requests like model discovery are blocked before transmission because provider URL hostname opencode.ai resolves to private-network address (fdfe:dcba:9876::7e). This causes the web dashboard to permanently display the warning badge Discovery failed and degrades to static presets.

Normal chat/completions through the proxy succeed because the inference path relies on upstream proxy tunneling (CONNECT), whereas the management discovery path strictly enforces local DNS pre-flight safety checks via resolvePublicAddresses(), causing an observable inconsistency between inference and discovery.

Reproduction

  1. Start Clash Verge / Mihomo with enhanced-mode: fake-ip and fake-ip-range6: fdfe:dcba:9876::1/64 on a dual-stack IPv6 network.
  2. Start OpenCodex: ocx start --port 10100 with HTTP proxy configured to http://127.0.0.1:7897.
  3. Configure an external provider such as opencode-go with baseUrl: https://opencode.ai/zen/v1.
  4. Open the OpenCodex dashboard (http://localhost:10100/#models) or run ocx sync-cache.
  5. Observe that discovery fails with: Provider model discovery for "opencode-go" was blocked by destination policy: provider URL hostname opencode.ai resolves to private-network address (fdfe:dcba:9876::7e).

Version

2.42.0

Operating system

macOS 26.6.2

Provider and model

opencode.ai / opencode-go

Logs or error output

[opencodex] Provider model discovery for "opencode-go" was blocked by destination policy: provider URL hostname opencode.ai resolves to private-network address (fdfe:dcba:9876::7e)

Additional context & Root Cause Analysis

In src/lib/destination-policy.ts, isBenchmarkDnsAnswer() validates the IPv4 benchmark space 198.18.0.0/15 (introduced in #1748), but omits the de-facto community standard Mihomo IPv6 Fake-IP ULA prefix fdfe:dcba:9876::/48 (RFC 4193).

Why fdfe:dcba:9876::/48?
In Mihomo (formerly Clash.Meta), fake-ip-range6: fdfe:dcba:9876::1/64 is the official community default and is adopted by virtually all modern Clash Verge Rev profiles and subconverters. Because it counts down (FE -> DC -> BA -> 98 -> 76), it is easily distinguishable and poses near-zero risk of overlapping with real enterprise ULA deployments.

Security Consideration:
isBenchmarkDnsAnswer() is only evaluated when allowBenchmarkAddresses is enabled (i.e. proxyConfigured && !noProxyMatches(parsed)). Therefore, admitting this IPv6 prefix will NOT weaken SSRF protections for direct connections or local network targets; it only applies when the request rides the configured outbound proxy.

Proposed Fix:
In src/lib/destination-policy.ts:

 function isBenchmarkDnsAnswer(address: string, assessment: DestinationAssessment | null): boolean {
   if (assessment?.kind === "private" && assessment.detail === "benchmark address") return true;
   if (isIP(address) !== 6) return false;
+  // Support Clash / Mihomo standard fake-ip-range6 (fdfe:dcba:9876::/48)
+  if (address.toLowerCase().startsWith("fdfe:dcba:9876:")) return true;
   if (assessment?.kind !== "private" || assessment.detail !== "non-global address") return false;
   const hextets = ipv6Hextets(normalizeHostname(address));

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. github-actions commented on Sep 4, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  2. added
    bugSomething isn't working
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Sep 4, 2026
  3. lidge-jun commented on Sep 4, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 58 / 80

    설명

    이 이슈는 Clash Verge / Mihomo의 IPv6 Fake-IP 모드에서 모델 discovery만 destination policy에 막히는 버그입니다. opencode.ai 같은 공개 호스트가 fdfe:dcba:9876::…로 풀리면, 관리 경로의 resolvePublicAddresses() / destination policy가 그걸 private-network로 보고 discovery를 차단합니다. 대시보드는 “Discovery failed” 뱃지로 정적 프리셋에 떨어지고, 같은 환경에서 채팅/completions은 CONNECT 터널로 잘 됩니다. 보고자가 말한 추론 경로와 discovery 경로의 불일치가 핵심입니다.

    지금 dev의 src/lib/destination-policy.ts를 보면 Fake-IP 예외는 allowBenchmarkAddresses 옵트인 안에만 있습니다. IPv4 198.18.0.0/15(#1748), NAT64 64:ff9b::/96(#2839/#2798), 그리고 명시적 제로 매핑 ::ffff:0:<hi>:<lo> 꼬리가 벤치마크일 때만 통과시키는 분기(#2842, isBenchmarkDnsAnswer 약 135–146행)가 있습니다. 커뮤니티 기본 IPv6 Fake-IP 접두사 fdfe:dcba:9876::/48는 어느 분기에도 없습니다. ULA라서 classifyIpv6가 non-global address로 분류하고, 벤치마크 detail도 아니며, explicit-zero 매핑도 아니라서 옵트인이 켜져 있어도 막힙니다. 보고의 RCA와 코드가 일치합니다.

    보안 이야기도 타당합니다. allowBenchmarkAddresses는 아웃바운드 프록시가 설정돼 있고 no_proxy에 안 걸릴 때만 켜지는 좁은 예외입니다. 직접 연결·로컬 타깃 SSRF 방어를 풀지 않습니다. 다만 제안 diff처럼 startsWith("fdfe:dcba:9876:") 문자열 접두만 넣으면, hextet 정규화·축약 표기(fdfe:dcba:9876::7e vs 전체 펼침)와 다른 /64·커스텀 range 사용자까지 어떻게 다룰지 설계가 필요합니다. 또 Mihomo 기본값이 “사실상 표준”이어도, 제품이 특정 벤더 ULA를 하드코딩하는 선례를 남길지 메인테이너 판단이 필요합니다.

    types/config 분할과 무관하고, 프록시·destination-policy 스택의 작은 독립 수정입니다. 영향 범위는 Clash IPv6 Fake-IP 사용자로 좁지만, discovery만 깨지고 채팅은 되는 증상은 디버깅 비용이 큽니다.

    라인 135 - isBenchmarkDnsAnswer가 IPv4 벤치마크·explicit-zero 매핑만 인정합니다. fdfe:dcba:9876::/48은 빠져 있습니다.
    경로 classifyIpv6 - ULA/non-global로 분류해 일반 SSRF 거절에 넣습니다. Fake-IP 예외는 여기가 아니라 allowBenchmarkAddresses 쪽에서만 열어야 합니다.
    경로 allowBenchmarkAddresses 호출부 - discovery/management preflight가 이 플래그를 켜는 조건(proxyConfigured && !noProxy)과 보고 환경이 맞는지 PR에서 테스트로 고정해야 합니다.
    제안 startsWith("fdfe:dcba:9876:") - 축약 IPv6·대소문자·다른 fake-ip-range6 값에 대한 경계 테스트가 필요합니다. hextet 비교가 문자열 접두보다 안전할 수 있습니다.

    메인테이너의 판단이 필요한 지점

    • Mihomo 기본 ULA fdfe:dcba:9876::/48만 허용할지, 설정 가능한 Fake-IP IPv6 접두사 목록으로 갈지
    • 문자열 접두 허용 vs hextet//48 마스크 비교 중 어떤 계약을 고정할지
    • discovery만 풀지, 동일 정책이 적용되는 다른 management fetch에도 같은 예외를 일괄 적용할지
    • 문서에 “Clash IPv6 Fake-IP면 allowBenchmarkAddresses + 이 접두”를 공식 지원으로 적을지

    너의 추천
    버그로 유지하고 작은 destination-policy PR을 받는 게 좋습니다. 범위는 allowBenchmarkAddresses 경로에서만 fdfe:dcba:9876::/48을 인정하고, hextet 단위 /48 매칭 + 축약/대문자 케이스 테스트, 그리고 “프록시 없을 때는 여전히 거절” 회귀를 넣으세요. 설정 가능한 접두사 목록은 나중에 필요하면 열고, 첫 PR은 커뮤니티 기본값만으로도 충분합니다. types/config 분할에 걸려 close할 이슈는 아닙니다.

    이 댓글은 grok-bot이 작성했습니다

  4. lidge-jun commented on Sep 5, 2026

    @lidge-jun
    Owner

    Closing as implemented on dev by #3615 (commit 7a704e3b078f1a92b81c0f7878a57cf881ca546b), with the complementary canonical-discovery path preserved in #3618.

    Mihomo fdfe:dcba:9876::/48 DNS answers are now admitted only when a scheme-matched outbound proxy is captured before DNS and bound to the actual request. Direct connections, NO_PROXY bypasses, literal private destinations, and unrelated private ranges retain their rejection behavior. The landed destination-policy and provider-outbound regressions cover these boundaries.

    Verified that the landing commit is an ancestor of current dev af50c6d3451078a7d298b044c08fd2684c9e8eeb. Cross-platform CI passed for that exact dev commit (Linux and macOS test shards; the Windows full-suite job was skipped).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions