feat(core): add REALITY target scanner - #695
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughAdds a REALITY target scanning workflow across backend models, public-network TLS probing, authenticated API handling, frontend service integration, editor controls, result visualization, and unit coverage. ChangesReality scan workflow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant XrayInboundsSection
participant RealityScanDialog
participant BackendAPI
participant scan_reality_target
participant TLSProbes
XrayInboundsSection->>RealityScanDialog: Open with target and initial SNI
RealityScanDialog->>BackendAPI: Submit target, SNI, and timeout
BackendAPI->>scan_reality_target: Resolve and scan target
scan_reality_target->>TLSProbes: Run TLS, group, and HTTP/3 probes
TLSProbes-->>scan_reality_target: Negotiation and certificate metadata
scan_reality_target-->>BackendAPI: RealityScanResult
BackendAPI-->>RealityScanDialog: Return scan result
RealityScanDialog-->>XrayInboundsSection: Render feasibility and probe details
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Note Unit test generation is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/utils/reality_scan.py`:
- Around line 549-554: Move _resolve_public_ip_async inside the
_get_scan_semaphore context in scan_reality_target, keeping DNS_TIMEOUT clamping
and the subsequent _scan_sync call intact. Update
tests/test_reality_scan_unit.py lines 390-406 in test_scan_concurrency_is_capped
to track and assert resolver concurrency is capped by the same semaphore.
In `@dashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsx`:
- Around line 62-69: Update the dialog reset effect and runScan flow to cancel
any in-flight scan when the dialog closes or resets. Reuse the existing signal
parameter in runScan, abort the previous request before clearing state, and
ensure stale scan completions cannot update result, errorMessage, or isScanning
for the new target.
In
`@dashboard/src/features/core-editor/components/xray/xray-inbounds-section.tsx`:
- Around line 4012-4016: The REALITY scanner must preserve the configured SNI
when scanning a target. In
dashboard/src/features/core-editor/components/xray/xray-inbounds-section.tsx
lines 4012-4016, extract the configured server name alongside the target and
pass it when opening the scanner; in
dashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsx lines
71-81, accept that SNI and include it in the scanRealityTarget request.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 71c9437b-f3a3-4b3d-9741-832ce2e49795
📒 Files selected for processing (8)
app/models/reality_scan.pyapp/operation/core.pyapp/routers/core.pyapp/utils/reality_scan.pydashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsxdashboard/src/features/core-editor/components/xray/xray-inbounds-section.tsxdashboard/src/service/reality-scan.tstests/test_reality_scan_unit.py
|
Request timed out after 900000ms (requestId=715f7044-de8a-4e6d-8556-e73d11375206) |
Add a POST /api/core/reality-scan endpoint and a "Scan target" button in the Xray inbound editor to probe a candidate REALITY dest and report whether it is a good decoy: TLS 1.3, HTTP/2 (ALPN), X25519 / post-quantum (X25519MLKEM768) key exchange, HTTP/3 advertisement, certificate validity and SANs, plus handshake latency. Security: every target is resolved and pinned to a public IP, so private, loopback, link-local, reserved and cloud-metadata addresses are rejected and the pinned connection cannot be bypassed by DNS rebinding. DNS and concurrent scans are bounded, and each sub-probe is isolated so a partial failure degrades a single field to "unknown" instead of failing the scan. The key-group probe is a self-contained raw TLS 1.3 ClientHello/ServerHello parser, so it detects MLKEM even where the panel's OpenSSL predates it. Gated behind the cores:read permission. Includes backend unit tests.
17381e7 to
0a94cd8
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
- _h3_probe: bound the response read with an absolute deadline (was a per-recv timeout only) and cap the whole scan with asyncio.wait_for, so a slowloris target cannot pin a scan slot/thread for a long time. - parse_target: reject control characters (CR/LF/NUL/tab) in target and sni, preventing header injection into the h3 probe request. - dashboard: coerce a non-string FastAPI 422 detail (array/object) to text in getErrorMessage so a validation error cannot crash the dialog render. - dashboard: when every REALITY serverName is a wildcard, scan without an SNI instead of sending an invalid "*.host" SNI.
Turn the REALITY scanner into a batch tool: add targets as chips (type and press space, comma, or enter; paste a whitespace/comma/newline list) and each is probed against the existing /api/core/reality-scan endpoint, bounded to a small client-side concurrency pool (the backend semaphore already caps real concurrency). Results stream in as compact per-target rows (verdict, TLS 1.3 / HTTP/2 badges, latency) that expand to the full detail card, with a "suitable only" filter and a running suitable/total count. A single target keeps the original detailed view. Frontend-only; no new endpoint or backend change, and reuses the abort/request-owner guard.
- Stop now optimistically clears the scanning state (and detaches the run) so control returns immediately even though the shared fetch client drops the abort signal; the request-owner guard still ignores any late results. - The targets chip input is fully inert during a scan: the text input and each chip's remove button are disabled, not just pointer-events, so a chip cannot be removed by keyboard mid-scan. - The input stays editable at the target cap (no frozen draft) and a hint shows when the maximum is reached.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@dashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsx`:
- Around line 361-374: Update the effect watching open in RealityScanDialog so
transitioning to false aborts the active controller and clears abortRef.current.
Preserve the existing reset behavior when open is true, ensuring scans stop
immediately on close while retaining unmount cleanup as a safeguard.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f5fb7057-21f6-4ace-b549-cf0527a2d174
📒 Files selected for processing (8)
app/models/reality_scan.pyapp/operation/core.pyapp/routers/core.pyapp/utils/reality_scan.pydashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsxdashboard/src/features/core-editor/components/xray/xray-inbounds-section.tsxdashboard/src/service/reality-scan.tstests/test_reality_scan_unit.py
The reset effect only aborted the running scan when the dialog opened; on close the parent keeps the component mounted, so the unmount cleanup never ran and probes kept hitting targets and patching state in the background. Abort and clear the scanning state on open -> false too.
|
@coderabbitai review |
✅ Action performedReview finished.
|
The scanner strings previously relied on t() defaultValue fallbacks and
were English-only. Add proper locale keys for all 31
coreEditor.realityScan.* strings plus coreConfigModal.scanRealityTarget
("Scan target") to en/fa/ru/zh.
Placeholders ({{max}}, {{tag}}, {{feasible}}, {{total}}) and protocol
tokens (TLS 1.3, HTTP/2, Alt-Svc, X25519, X25519MLKEM768) are kept
verbatim across locales.
|
the scanner should test the IP and SNI together, not just the IP on its own. so like, take 1.1.1.1:443 with SNI example.com, try the handshake with those two together and check if it comes back TLS 1.3 and if the cert actually matches that SNI. if both work then that pair is good to use. also worth knowing a different SNI on the same IP might not work the same way, so each pair kinda needs its own test |
yeah it already does exactly this. the whole point of reality is to mimic the TLS, it's not even about the ip, and that's what the scan actually checks. it does the tls 1.3 handshake with the sni you give it and validates the cert against that sni, so the sni is what decides it, not the ip. (the SAN output is just a cherry on top) look at the two tests, same endpoint (1.0.0.1): as a bare ip with no sni it returns "not suitable" (nothing for the cert to match), but as one.one.one.one with the sni it returns "suitable".
and you can check it yourself, same ip different sni: (get an api key or use the owner user/pass) which returns: in the UI it's resolving the ip from the target and checks the cert against the sni (target), and the results are based on that. that's the whole point. and you might be wondering, why H3 and post quantum then? the h3 check is a nice bonus if you're also picking a host for hysteria masquerade (that fronts a real http/3 site), and the post quantum check matters because modern browser + cloudflare negotiate x25519mlkem768 now, so it flags whether a decoy matches a current browser fingerprint and whether you'd want to add pq or not.
|
…700) * refactor(core): derive REALITY scan SNI from the target The scanner no longer takes a separate SNI and no longer pulls the inbound's first serverName. The SNI is derived from the target itself: - hostname target -> the hostname is its own SNI, validated against it - bare IP target -> handshake, discover the domain from the presented certificate (CN or first non-wildcard SAN), then re-validate the cert against that discovered domain; the result reports it as discovered from the IP Removes RealityScanRequest.sni and the serverNames->SNI dialog wiring; adds RealityScanResult.sni_discovered. Multi-target scanning is kept (each target derives its own SNI). Adds sniLabel/sniDiscovered locales. * fix(core): harden the REALITY scanner (handshake deadline, per-loop semaphore) - Bound the TLS handshake with an absolute deadline (non-blocking socket + select loop) so a slow-drip peer can no longer keep do_handshake() running forever, and run scans on a dedicated ThreadPoolExecutor so a lingering scan thread can never starve the event loop's shared pool (getaddrinfo / other to_thread work). - Key the concurrency semaphore per running event loop via a WeakKeyDictionary, removing the cross-loop "bound to a different event loop" RuntimeError. - Preserve the certificate reason when the permissive fallback also fails, handle UnicodeError from over-long DNS labels gracefully instead of a 502, and gate the discovered SNI through a hostname sanity check. * fix(core): address PR #700 review on the REALITY scanner Prefer non-wildcard DNS SANs over the certificate CN when discovering a bare-IP target's server name, and fall back to the CN only when the certificate carries no DNS SANs. This matches OpenSSL hostname verification, which ignores the CN whenever any dNSName SAN is present, so a CN-derived name no longer fails re-validation on an otherwise valid target. Require a confirmed X25519-family key exchange for feasibility instead of only rejecting a confirmed non-X25519 curve. A group probe that cannot determine the curve now marks the target not feasible with an explicit reason rather than passing on an unverified assumption. * test(core): reuse the cert helper for no-SAN scanner cases Let _self_signed_der skip the SubjectAlternativeName extension when no SANs are given, and collapse the three inline no-SAN certificate builders onto it, addressing the PR #700 review nitpick. --------- Co-authored-by: parsa <me@local.test>




Summary
Adds a REALITY target scanner: a "Scan target" button in the Xray inbound
editor (next to a REALITY
dest/targetfield) that probes a candidatehandshake target and reports whether it is a good REALITY decoy, plus a
POST /api/core/reality-scanendpoint behind it.For a given host it checks:
(surfaced as suggested valid SNIs)
The verdict (
feasible) requires the hard REALITY requirements: TLS 1.3 +HTTP/2 + a valid certificate, and it is not vetoed unless a non-X25519 group
was definitively observed. Post-quantum and HTTP/3 are reported as extras and
never fail the scan.
How it works
Three isolated probes, so a partial failure degrades one field to "unknown"
instead of failing the whole scan:
ssl(TLS version, ALPN/H2, certificate parsedwith
cryptography).negotiated key group. This is done by hand because the panel's OpenSSL 3.0
predates X25519MLKEM768 (OpenSSL 3.5), so it cannot rely on the local stack.
Alt-Svcheader.Security
The endpoint could otherwise be an SSRF primitive, so:
link-local, reserved and cloud-metadata addresses are rejected.
the DNS-rebinding hole.
cores:readpermission.API
POST /api/core/reality-scan->RealityScanResultRequest:
{ target, sni?, timeout? }(timeout clamped to 1-20s, default 10).Tests
Backend unit tests in
tests/test_reality_scan_unit.pycover target parsing,the SSRF public-host guard, the ClientHello/ServerHello group probe,
certificate/SAN parsing, and the feasibility logic (no network required; one
opt-in network integration test behind
REALITY_SCAN_NETWORK_TEST=1).Notes
t(key, { defaultValue: '...' }), so they render in English and fall backcleanly with translations missing. Adding
fa/zh/ruentries undercoreEditor.realityScan.*(andcoreConfigModal.scanRealityTarget) is agood follow-up.
service/reality-scan.ts)instead of the generated one, mirroring the generated call shape. It can be
folded into the generated API client once codegen is re-run.
Summary by CodeRabbit
/api/core/reality-scan), returning structured TLS/HTTP/feasibility and certificate metadata.