Skip to content

fix(discovery): admit Mihomo IPv6 fake-IP DNS answers behind a scheme-matched proxy - #3551

Closed
lidge-jun wants to merge 5 commits into
codex/3467-google-location-errorfrom
codex/3462-mihomo-ipv6-fakeip
Closed

lidge-jun wants to merge 5 commits into
codex/3467-google-location-errorfrom
codex/3462-mihomo-ipv6-fakeip

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

Under Clash Verge / Mihomo with Fake-IP on a dual-stack IPv6 network, provider hostnames resolve into Mihomo's default fake-ip-range6 fdfe:dcba:9876::/48. That prefix is ULA, so resolvePublicAddresses classified every answer as a private-network address and model discovery failed behind the destination policy ("Discovery failed" badge, static presets) while ordinary chat traffic — which rides the proxy as a CONNECT — kept working (#3462).

The existing IPv4 benchmark exception (198.18/15, #1748) could not cover this: a ULA answer carries no signal that it is synthetic, so it gets a narrower gate than benchmark space.

  • src/lib/destination-policy.ts: new allowMihomoIpv6FakeIp option on resolvePublicAddresses — exact /48 match, DNS answers only. Literal ULA URLs, adjacent prefixes, ordinary ULA, loopback/metadata/RFC1918, and fake-IP mixed with a real private answer still reject. classifyIpv6 and providerDestinationResolvedError (config-time validation) are unchanged, so the canonical-openai benchmark opt-in in provider-routes.ts never admits the ULA.
  • src/lib/proxy-env.ts: effectiveProxyFor(url) returns the proxy variable Bun fetch will actually honour for the URL scheme (HTTPS_PROXY for https:, HTTP_PROXY for http:); ALL_PROXY is not consulted. Presence of some proxy variable was not a routing guarantee.
  • src/lib/provider-outbound.ts: the flag is derived from that scheme-matched proxy plus !NO_PROXY, snapshotted before the DNS await; when the flag admitted an answer, the fetch is bound to the same proxy through Bun's explicit proxy option (BunFetchRequestInit.proxy) rather than environment inference. Every other branch is byte-identical to today.
  • Docs: providers.md §"Provider diagnostic outbound safety" in English plus ko/ja/zh-cn/zh-tw/fr/ru/tr.

Related: the open pull request for the no-proxy TUN case on canonical endpoints does not recognise the IPv6 range; this change is independent of it and neither replaces the other.

Security review (MAINTAINERS.md): this touches the SSRF destination boundary. Reviewer focus: the admission gate is only ever true when a scheme-matched proxy exists and the host is not in NO_PROXY, and the transport is then pinned to that proxy — a hostname resolving to a genuinely reachable fdfe:dcba:9876::/48 host cannot be connected directly. No credential, auth-flow, or logging change.

Stack (260905 bug triage, position 2 of 5; base is #3547's head — retarget to dev after #3547 lands):

# PR Layer Base
1 #3547 #3467 Google location classification dev
2 this #3462 Mihomo IPv6 fake-ip discovery #3547
3 next #3464 launchd stable launcher this head
4 #3542 #3522 spill write health
5 next #3406 truthful Codex toggle 3

Unit: devlog/_plan/260905_bug_triage_stack/ (000, 005, 020).

Verification

  • bun run typecheck — exit 0.
  • bun test tests/routing/destination-policy-resolved.test.ts tests/providers/provider-outbound.test.ts — 71 pass / 0 fail. RED-before: with the three source files stashed, 27 new assertions fail; GREEN-after.
  • New coverage: accept under flag (compressed/uppercase/expanded/non-zero 4th hextet); reject without flag; benchmark-only flag does not admit ULA; literal ULA rejected; adjacent /48s + fd00::/8 + loopback + metadata + RFC1918 rejected; mixed answers rejected; config-validation path unchanged; proxy matrix HTTPS+HTTPS_PROXY (admit + proxy option captured), lowercase, HTTPS+HTTP_PROXY-only (deny), ALL_PROXY-only (deny), NO_PROXY match (deny), no proxy (deny); effectiveProxyFor unit matrix.
  • bun run privacy:scan — passed.
  • Hosted CI on the exact head is the acceptance gate (no repository-wide local suite was run by maintainer instruction).

Closes #3462

Checklist

jun added 3 commits September 5, 2026 08:09
…-matched proxy

Under Clash Verge / Mihomo with Fake-IP on a dual-stack network, provider hostnames
resolve into Mihomo's default fake-ip-range6 fdfe:dcba:9876::/48. That prefix is ULA, so
resolvePublicAddresses classified every answer as a private-network address and model
discovery failed behind the destination policy while ordinary chat traffic — which rides
the proxy as a CONNECT — kept working (#3462).

The IPv4 benchmark exception (198.18/15) could not cover this: a ULA answer carries no
signal that it is synthetic, so it gets a narrower gate than benchmark space.

- destination-policy: new allowMihomoIpv6FakeIp option, exact /48 match, DNS answers only;
  literal ULA URLs, adjacent prefixes, ordinary ULA and mixed private answers still reject.
  classifyIpv6 and config-time validation are unchanged, so the canonical-openai benchmark
  opt-in in provider-routes never admits the ULA.
- proxy-env: effectiveProxyFor(url) returns the variable Bun fetch will honour for the URL
  scheme (HTTPS_PROXY / HTTP_PROXY); ALL_PROXY is not consulted. Proxy presence alone was
  not a routing guarantee.
- provider-outbound: the flag is derived from that scheme-matched proxy plus !NO_PROXY, and
  when the flag admitted an answer the fetch is bound to the same proxy via Bun's explicit
  proxy option instead of environment inference. Every other branch is byte-identical.

Docs: providers.md "Provider diagnostic outbound safety" (en + ko/ja/zh-cn/zh-tw/fr/ru/tr).

Closes #3462
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 4, 2026 23:16
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 1f0c8b26-9a46-4343-b1dd-312dcc6b3169

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • missing_coauthor_credit — This pull request says it reimplements, supersedes, carries, or rebases another author's pull request, but no Co-authored-by trailer names that author. Prose in a commit body is not read by anything; the trailer is what GitHub counts. Add it to the description or a commit, or obtain attribution-approved. Paths: #3489.

@github-actions github-actions Bot added intake: hygiene-blocked Deterministic PR hygiene checks failed bug Something isn't working labels Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

Hygiene

Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft September 4, 2026 23:18
@lidge-jun
lidge-jun marked this pull request as ready for review September 4, 2026 23:20
@github-actions github-actions Bot removed the intake: hygiene-blocked Deterministic PR hygiene checks failed label Sep 4, 2026

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security direction is appropriately narrow on this stacked head: only DNS answers in the exact fdfe:dcba:9876::/48 prefix can use the exception; literal IP URLs, adjacent ULA ranges, mixed private answers, and config-time validation remain blocked. More importantly, the admission snapshots a scheme-matched proxy before DNS and the admitted request is explicitly bound to that same proxy, so it cannot fall back to a direct connection after accepting an otherwise-private answer. NO_PROXY remains a denial for this exception.

I am requesting changes only because this security-boundary PR currently targets codex/3467-google-location-error, and #3547 is still blocked for error-precedence fixes. The repository accepts integration PRs against dev (or scoped Go work against dev2-go), and a security approval must be for the final exact diff/base—not a temporary stack parent that may change.

After #3547 is corrected and lands, retarget/rebase this PR onto the then-current dev, remove inherited stack-only files from the final diff, clear the current hygiene/attribution state, and run exact-head CI plus a fresh security review. Do not merge it into the temporary parent branch.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 66 / 80

이 PR는 Clash Verge / Mihomo에서 IPv6 Fake-IP를 켠 이중 스택 환경에서, 모델 디스커버리만 막히던 문제를 고칩니다. 지금 dev(HEAD 1362b1a38)의 resolvePublicAddresses는 호스트네임이 Mihomo 기본 fake-ip-range6fdfe:dcba:9876::/48로 풀리면 ULA(사설망)로 분류합니다. 그래서 destination policy가 막히고 Discovery failed 배지가 뜹니다. 같은 환경에서 일반 채팅은 CONNECT로 프록시를 타서 잘 됩니다. 이게 이슈 #3462입니다.

IPv4 쪽에는 이미 #1748로 198.18/15 벤치마크 예외(allowBenchmarkAddresses)가 있습니다. 그런데 IPv6 ULA 답은 “가짜 IP”라는 표시가 주소 자체에 없어서, IPv4 예외를 그대로 넓히면 진짜 사설 호스트까지 열릴 수 있습니다. 그래서 이 PR는 더 좁은 문을 새로 엽니다. destination-policy.tsallowMihomoIpv6FakeIp를 넣고, DNS 답만, 정확히 그 /48만 통과시킵니다. 리터럴 ULA URL, 옆 /48, 일반 ULA, 루프백/메타데이터/RFC1918, fake-IP와 진짜 사설이 섞인 답은 그대로 거절합니다. classifyIpv6와 설정 시점 검증(providerDestinationResolvedError)은 안 건드립니다. 그래서 provider-routes.ts의 canonical-openai 벤치마크 opt-in이 이 ULA를 절대 받지 않습니다.

프록시 쪽도 같이 손봤습니다. proxy-env.tseffectiveProxyFor(url)는 Bun fetch가 실제로 쓰는 변수만 봅니다. https:HTTPS_PROXY, http:HTTP_PROXY입니다. ALL_PROXY는 고의로 안 봅니다. “프록시 변수가 하나라도 있다”(outboundProxyConfigured)와 “이 URL이 진짜로 프록시를 탄다”는 다른 말입니다. provider-outbound.ts는 DNS 전에 그 값을 스냅샷하고, 스킴 맞는 프록시가 있고 NO_PROXY에 안 걸릴 때만 Mihomo 플래그를 켭니다. 예외로 답을 받은 뒤에는 Bun의 명시 proxy 옵션으로 같은 프록시에 붙입니다. 환경변수 추론에만 맡기지 않습니다. 다른 분기는 지금과 바이트 단위로 같습니다.

관련 이슈 #3462는 아직 OPEN이고, 베이스는 #3547(codex/3467-google-location-error) 헤드입니다. 스택 2/5 자리이고, #3547이 dev에 들어간 뒤 리타깃해야 합니다. no-proxy TUN용 다른 PR(#3489 계열)과는 범위가 겹치지 않습니다. 테스트는 destination-policy / provider-outbound에 집중 회귀가 있고, 문서도 providers.md와 7개 로케일을 갱신했습니다. SSRF 경계를 건드리는 변경이라 MAINTAINERS 보안 리뷰 포인트로 잘 표시돼 있습니다.

라인 157-164 (destination-policy.ts isMihomoIpv6FakeIpAnswer) - Mihomo 기본 /48만 허용합니다. 사용자가 fake-ip-range6를 다른 대역으로 바꿨으면 여전히 막힙니다. 의도된 좁은 문인지, 설정 가능 목록이 필요한지 한 줄 문서화가 더 있으면 좋습니다.

라인 effectiveProxyFor (proxy-env.ts) - IPv4 벤치마크 예외는 여전히 outboundProxyConfigured(ALL_PROXY 포함)로 열리는데, IPv6 Mihomo 예외는 스킴 매칭 프록시만 봅니다. ALL_PROXY만 있는 사용자는 IPv4 fake-IP는 되고 IPv6는 안 됩니다. Bun fetch 동작과 맞춘 비대칭이라 맞지만, 증상 설명이 docs에 있으면 덜 헷갈립니다.

라인 provider-outbound.ts fetch proxy 핀 - allowMihomoIpv6FakeIp가 켜져 있으면 fake-IP를 실제로 받지 않은 일반 공인 주소 discovery fetch에도 명시 proxy가 붙습니다. “예외가 켜질 수 있는 조건이면 수송을 고정한다”는 보안 선택이라 타당하지만, 범위가 “Mihomo를 실제로 인정한 경우만”보다 넓다는 점은 알아둘 만합니다.

경로 devlog/_plan/260905_bug_triage_stack/* - 스택 계획·연구 문서가 런타임 diff와 같이 들어 있습니다. 머지 전 정리할지, 스택 전체 공용 문서로 둘지 선택입니다.

경로 hygiene missing_coauthor_credit - 봇이 carry/rebase 문구를 보고 경고했을 수 있습니다. 본문 기준으로 #3462는 신규 수정이고 #3489를 가져오는 carry가 아닙니다. 트레일러가 정말 필요한지 한 번만 확인하면 됩니다.

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

  • #3547이 먼저 머지된 뒤에만 이 PR를 dev로 리타깃할지, 아니면 Google 위치 분류와 완전히 독립이니 베이스를 지금 dev로 바꿔 단독 랜딩할지
  • Mihomo 기본 /48만 하드코딩할지, 자주 쓰는 커스텀 fake-ip-range6를 허용 목록으로 둘지
  • ALL_PROXY-only 환경에서 IPv6 discovery 실패를 문서/진단 메시지로만 남길지, 제품 동작으로 더 맞출지
  • SSRF 리뷰 기준: “스킴 매칭 프록시 + 명시 pin + DNS-only /48”이면 충분한지

너의 추천
#3547 머지 후 베이스를 dev로 리타깃하고, CI(enforce-target 포함) 그린 확인 뒤 머지하세요. 코드 방향은 HEAD의 fake-IP/NAT64 정책(#2842, #2839/#2798)과 잘 맞고, #3462를 닫는 올바른 좁은 예외입니다. 커스텀 fake-ip 대역 지원은 이번 PR에 넣지 말고 follow-up으로 두세요. 단독으로 dev에 올리고 싶으면 런타임 세 파일+테스트+docs만 남기고 베이스를 dev로 바꾼 뒤 스택 형제와 분리해도 됩니다.

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

lidge-jun added a commit that referenced this pull request Sep 5, 2026
Owner-authorized admin squash of child-only #3551 carry onto corrected #3608. Scheme-matched proxy snapshot, explicit proxy transport binding, exact IPv6 prefix and NO_PROXY denial retained. Existing source security assessment and current static/typecheck evidence recorded. No local tests; final dev Linux CI is the batch gate.
@lidge-jun

Copy link
Copy Markdown
Owner Author

Carried into dev by #3615 at 7a704e3, with original attribution retained and the concrete follow-up corrections described there. Closing the source PR as superseded. Final dev HEAD CI is still pending under the owner-authorized admin-merge workflow; this closure does not claim CI success.

@lidge-jun lidge-jun closed this Sep 5, 2026
@lidge-jun

Copy link
Copy Markdown
Owner Author

Landed via #3615 at 7a704e3

@lidge-jun lidge-jun added the landed-via-maintainer Original PR closed after landing via a maintainer merge train label Sep 5, 2026
@lidge-jun
lidge-jun deleted the codex/3462-mihomo-ipv6-fakeip branch September 5, 2026 09:03
agentHits pushed a commit to agentHits/opencodex that referenced this pull request Sep 17, 2026
…ge-jun#3615)

Owner-authorized admin squash of child-only lidge-jun#3551 carry onto corrected lidge-jun#3608. Scheme-matched proxy snapshot, explicit proxy transport binding, exact IPv6 prefix and NO_PROXY denial retained. Existing source security assessment and current static/typecheck evidence recorded. No local tests; final dev Linux CI is the batch gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working landed-via-maintainer Original PR closed after landing via a maintainer merge train

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants