Skip to content

[Bug]: fake-IP allowance unreachable for IPv6-mapped answers — classifyIpv6 hex regex misses the explicit zero group (::ffff:0:c612:1b) #2810

Closed

Description

@gaoran1209

Client or integration

Codex App

Area

Proxy and routing

Summary

On machines using Clash / Surge / Mihomo fake-IP DNS, provider model discovery still fails with blocked by destination policy: ... resolves to non-global address, even though #1748 added the fake-IP allowance.

The root cause is that the fake-IP opt-in in resolvePublicAddresses() is unreachable for IPv6-mapped answers. classifyIpv6() decodes hex IPv4-mapped IPv6 with:

const hexMapped = hostname.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);

That pattern assumes exactly two hex groups (::ffff:c612:1b). But the resolver here returns an explicit zero group:

cli-chat-proxy.grok.com has address 198.18.0.27
cli-chat-proxy.grok.com has IPv6 address ::ffff:0:c612:1b

::ffff:0:c612:1b does not match, so it falls through to the generic tail:

return { kind: "private", detail: "non-global address" };

It is therefore classified private with detail "non-global address" — not "benchmark address". The allowance in resolvePublicAddresses() is gated on that exact detail string:

if (benchmarkAllowed && assessment?.kind === "private" && assessment.detail === "benchmark address") {

so the branch never runs, and the request is rejected regardless of HTTP(S)_PROXY being correctly configured. Node's ipaddress-style parsers agree the address is not a canonical ipv4_mapped value, which is why it reaches the hex path at all.

Security note: this same gap means loopback/private/link-local addresses in that zero-group form also skip IPv4 classification today. See the table below — fixing the regex tightens the policy rather than loosening it.

Reproduction

  1. Run Clash / Surge / Mihomo with fake-IP DNS enabled (198.18.0.0/15).
  2. Export a working proxy, e.g. export HTTPS_PROXY=http://127.0.0.1:1082.
  3. Configure any provider whose hostname resolves through fake-IP (reproduced with xai, but it affects every provider on this machine).
  4. ocx restart then ocx provider test xai.

Observed:

xai: failed
upstream /models blocked by destination policy: provider URL hostname cli-chat-proxy.grok.com resolves to non-global address (::ffff:0:c612:1b)

Expected: discovery succeeds by riding the configured proxy as an ordinary hostname CONNECT, per #1748.

Note the destination is reachable — the block is purely the classification:

curl -x http://127.0.0.1:1082 https://cli-chat-proxy.grok.com/v1/models  ->  401 (expected unauthenticated response)

Inference through the proxy also works fine; only discovery is blocked, so the failure is masked by the configured-model fallback.

Suggested fix

Make the zero group optional in src/lib/destination-policy.ts:

-  const hexMapped = hostname.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
+  const hexMapped = hostname.match(/^::ffff:(?:0{1,4}:)?([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);

Verified locally against v2.33.0. Decoding behaviour before/after:

Input old new decoded intent
::ffff:0:c612:1b miss hit 198.18.0.27 fake-IP → allow via proxy
::ffff:c612:1b hit hit 198.18.0.27 unchanged
::ffff:0:7f00:1 miss hit 127.0.0.1 now correctly blocked
::ffff:0:c0a8:1 miss hit 192.168.0.1 now correctly blocked
::ffff:0:a9fe:a9fe miss hit 169.254.169.254 now correctly blocked
::ffff:0:0:c612:1b miss miss — 4 groups still rejected

After applying it, all seven configured providers recovered live discovery, and the seven blocked by destination policy warnings disappeared from ocx sync:

Provider before after
xai blocked connected — 2 models
anthropic blocked connected — 10 models
kimi blocked connected — 4 models
deepseek blocked connected — 3 models
minimax-cn blocked connected — 8 models
zhipu-bigmodel-coding blocked connected — 10 models
google-antigravity blocked connected — 6 models

Happy to send this as a PR with a regression test over the address matrix above if useful.

Version

2.33.0

Operating system

macOS 26.5.2 (arm64)

Provider and model

xai / grok-4.6 (reproduces on all configured providers)

Logs or error output

$ ocx sync
[opencodex] Provider model discovery for "minimax-cn" was blocked by destination policy: provider URL hostname api.minimaxi.com resolves to non-global address (::ffff:0:c612:24) [urlClass=provider-models, fallback=configured].
[opencodex] Provider model discovery for "xai" was blocked by destination policy: provider URL hostname cli-chat-proxy.grok.com resolves to non-global address (::ffff:0:c612:1b) [urlClass=provider-models, fallback=configured].
[opencodex] Provider model discovery for "google-antigravity" was blocked by destination policy: provider URL hostname daily-cloudcode-pa.googleapis.com resolves to non-global address (::ffff:0:c612:29) [urlClass=provider-models, fallback=configured].
[opencodex] Provider model discovery for "anthropic" was blocked by destination policy: provider URL hostname api.anthropic.com resolves to non-global address (::ffff:0:c612:28) [urlClass=provider-models, fallback=configured].
[opencodex] Provider model discovery for "zhipu-bigmodel-coding" was blocked by destination policy: provider URL hostname open.bigmodel.cn resolves to non-global address (::ffff:0:c612:26) [urlClass=provider-models, fallback=configured].
[opencodex] Provider model discovery for "deepseek" was blocked by destination policy: provider URL hostname api.deepseek.com resolves to non-global address (::ffff:0:c612:25) [urlClass=provider-models, fallback=configured].
[opencodex] Provider model discovery for "kimi" was blocked by destination policy: provider URL hostname api.kimi.com resolves to non-global address (::ffff:0:c612:27) [urlClass=provider-models, fallback=configured].

$ ocx sync   # after the one-line fix
[opencodex] Provider outbound proxy mode preserves Bun proxy/NO_PROXY routing and validates the URL plus available local DNS results.
   + 18 models appended to Codex catalog

Checks

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

Activity

  1. added
    bugSomething isn't working
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Aug 28, 2026
  2. lidge-jun commented on Aug 28, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 75 / 80

    설명

    이 이슈는 Clash / Surge / Mihomo fake-IP DNS를 쓰는 맥에서 provider ocx sync / ocx provider test가 전부 blocked by destination policy … resolves to non-global address로 죽는 버그다. #1748이 이미 allowBenchmarkAddresses로 198.18.0.0/15(벤치마크·fake-IP) 답을 허용하는데, 그 허용 분기가 detail === "benchmark address"에만 걸려 있다. 지금 dev(8d9e286)의 src/lib/destination-policy.ts classifyIpv6 헥스 IPv4-mapped 정규식은 /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i라서 ::ffff:c612:1b는 맞지만, 리졸버가 돌려 주는 ::ffff:0:c612:1b(명시적 0 그룹)는 안 맞는다. 그러면 85–90행의 classifyIpv4로 안 내려가고 108행 kind: "private", detail: "non-global address"로 떨어져, 356행 근처 fake-IP 허용 조건(detail === "benchmark address")에 영원히 안 걸린다. 그래서 HTTPS_PROXY가 올바르고 curl -x로도 401까지 가는데도 discovery만 막힌다. 이슈 작성자가 적은 표대로, 정규식을 /^::ffff:(?:0{1,4}:)?([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i로 고치면 fake-IP(198.18.x)는 다시 benchmark로 분류되고, ::ffff:0:7f00:1 / ::ffff:0:c0a8:1 / ::ffff:0:a9fe:a9fe처럼 예전엔 놓치던 loopback·사설·메타데이터 래핑도 이제 제대로 막힌다. 즉 이 픽스는 허용을 느슨하게 하는 게 아니라 분류를 정확히 해서 보안을 더 단단히 한다.

    같은 파일·같은 함수를 건드리는 열린 PR #2798(NAT64 64:ff9b::/96 임베디드 IPv4 디코드)은 접두사가 완전히 다른 축이다. #2798은 ipv6Hextets 확장기를 추가하지만 지금 diff는 이 헥스 ::ffff: 정규식을 고치지 않는다. 그래서 #2810은 #2798과 중복이 아니라 옆집 버그다. 다만 메인테이너가 ipv6Hextets로 IPv4-mapped 형태까지 통일 디코드하는 쪽이 더 깔끔하다고 보면, 한 줄 정규식 대신 #2798에 주소 행렬 테스트를 얹는 식으로 묶을 수는 있다. types.ts/config.ts 스플릿이나 preview 배포와는 무관하다. 재현·수정 검증·프로바이더 7개 복구 로그까지 이슈에 이미 있어서 PR만 받으면 된다.

    라인 85 (classifyIpv6 hexMapped 정규식) - ::ffff:0:c612:1b처럼 명시적 0 그룹이 있는 IPv4-mapped를 놓쳐 108행 non-global로 떨어진다. 제안대로 optional (?:0{1,4}:)?를 넣거나 hextet 확장기 경로로 동일 디코드가 필요하다.
    라인 356 (resolvePublicAddresses benchmarkAllowed 분기) - 허용이 detail === "benchmark address"에만 묶여 있어, 잘못 분류된 non-global fake-IP는 프록시가 있어도 절대 통과하지 못한다. 분류가 고쳐지면 이 분기는 그대로 써도 된다.
    경로 tests/destination-policy-resolved.test.ts (#1748 스위트) - 지금 케이스는 점표기/일반 벤치마크 위주라 ::ffff:0:c612:1b / ::ffff:0:7f00:1 행렬이 없다. 이슈 표의 6행을 회귀 테스트로 넣어야 다시 깨지지 않는다.
    경로 #2798 (ipv6Hextets + NAT64) - 같은 classifyIpv6를 고치지만 ::ffff:0:…는 건드리지 않음. 병합 순서만 조심하면 충돌은 작다.
    경로 재현 - Clash fake-IP + HTTPS_PROXY + ocx provider test xai(또는 ocx sync)에서 ::ffff:0:c612:… 로그가 뜨는지가 고정 재현 신호다.

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

    • 한 줄 정규식 핫픽스를 별도 PR로 빨리 받을지, #2798의 hextet 확장기에 IPv4-mapped(명시 0 그룹 포함)까지 묶어 한 번에 받을지.
    • 이슈 작성자(gaoran1209)에게 바로 PR을 부탁할지, 메인테이너가 주소 행렬 테스트만 얹어 직접 올릴지.
    • fix(outbound): route Clash fake-IP discoveries through the HTTP proxy #1748 문서/주석에 ::ffff:0:… 형태도 fake-IP 경로라는 한 줄을 남길지.

    너의 추천
    버그로 유지하고 작성자 PR을 받아라. 최소 변경은 라인 85 정규식 + tests/destination-policy-resolved.test.ts에 이슈 표 행렬( hit/miss + loopback/사설/메타데이터 차단)이다. #2798과 축이 다르니 막지 말고, 둘 다 머지하면 destination-policy의 IPv6 래핑 구멍이 한 번에 줄어든다. 우선순위 높은 proxy/SSRF 분류 버그다.

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

  3. gaoran1209 commented on Aug 28, 2026

    @gaoran1209
    ContributorAuthor

    Opened #2812 with the one-line regex fix plus the regression matrix from the report.

    Verified locally: tsc --noEmit clean, privacy:scan passed, and 55 pass / 0 fail across destination-policy-resolved, provider-outbound-private-network, and proxy-env. Reverting only the source file makes the 6 new assertions fail, so the tests genuinely guard the regression.

    On the maintainer decision points:

    One caveat: I could not run the full bun run test suite locally because the harness needs process/network permissions my environment denies, so I scoped verification to the files this change can affect and left the rest to CI.

  4. lidge-jun commented on Aug 28, 2026

    @lidge-jun
    Owner

    Fixed on dev by commit c986d1d (PR #2842).

    The allowBenchmarkAddresses fake-IP opt-in now reaches the explicit-zero IPv4-mapped spelling, so a Clash/Surge/Mihomo resolver answering ::ffff:0:c612:1b for 198.18.0.27 is accepted instead of refused.

    The decode lives in the benchmark gate rather than in classifyIpv6, deliberately: ::ffff:0:<hi>:<lo> is a reserved address under RFC 4291 whose tail merely resembles an IPv4, so declaring it equivalent would have admitted things like ::ffff:0:5db8:d822 (tail 93.184.216.34) as ordinary public destinations. Both directions are pinned by tests.

    Verified with tests/destination-policy-resolved.test.ts at 45 pass / 0 fail (9 new tests, driven RED first), plus an independent adversarial review of the SSRF boundary. Credit to @gaoran1209, who diagnosed this in #2812.

    This PR targeted dev, so GitHub did not auto-close the issue — closing manually.

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