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

fix(discovery): admit Mihomo IPv6 fake-IP DNS answers behind a scheme-matched proxy#3551
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
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