Repository navigation
[Bug] Model discovery blocked by destination policy under Clash/Mihomo IPv6 fake-ip (fdfe:dcba:9876::/48) #3462
Description
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
- Start Clash Verge / Mihomo with enhanced-mode: fake-ip and fake-ip-range6: fdfe:dcba:9876::1/64 on a dual-stack IPv6 network.
- Start OpenCodex: ocx start --port 10100 with HTTP proxy configured to http://127.0.0.1:7897.
- Configure an external provider such as opencode-go with baseUrl: https://opencode.ai/zen/v1.
- Open the OpenCodex dashboard (http://localhost:10100/#models) or run ocx sync-cache.
- 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
github-actions commentedon Sep 4, 2026
Issue reopened
The report now contains the information required by the automated check. Thanks for updating it.
lidge-jun commentedon Sep 4, 2026
리뷰 · 우선순위 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옵트인 안에만 있습니다. IPv4198.18.0.0/15(#1748), NAT6464: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::7evs 전체 펼침)와 다른/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 비교가 문자열 접두보다 안전할 수 있습니다.
메인테이너의 판단이 필요한 지점
fdfe:dcba:9876::/48만 허용할지, 설정 가능한 Fake-IP IPv6 접두사 목록으로 갈지/48마스크 비교 중 어떤 계약을 고정할지너의 추천
버그로 유지하고 작은 destination-policy PR을 받는 게 좋습니다. 범위는
allowBenchmarkAddresses경로에서만fdfe:dcba:9876::/48을 인정하고, hextet 단위/48매칭 + 축약/대문자 케이스 테스트, 그리고 “프록시 없을 때는 여전히 거절” 회귀를 넣으세요. 설정 가능한 접두사 목록은 나중에 필요하면 열고, 첫 PR은 커뮤니티 기본값만으로도 충분합니다. types/config 분할에 걸려 close할 이슈는 아닙니다.이 댓글은 grok-bot이 작성했습니다
docs(structure): record the Mihomo IPv6 fake-IP admission rule beside…
lidge-jun commentedon Sep 5, 2026
Closing as implemented on
devby #3615 (commit7a704e3b078f1a92b81c0f7878a57cf881ca546b), with the complementary canonical-discovery path preserved in #3618.Mihomo
fdfe:dcba:9876::/48DNS 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
devaf50c6d3451078a7d298b044c08fd2684c9e8eeb. Cross-platform CI passed for that exact dev commit (Linux and macOS test shards; the Windows full-suite job was skipped).test(proxy): treat HTTP ALL_PROXY as unused after SOCKS5 rebase
fix(transport): decide DNS pinning by whether the proxy applies to th…
fix(security): request-scoped transport pinning and managed-write sym…