Skip to content

feat(core): add REALITY target scanner - #695

Merged
ImMohammad20000 merged 7 commits into
PasarGuard:devfrom
parsa222:reality-scanner
Jul 15, 2026
Merged

feat(core): add REALITY target scanner#695
ImMohammad20000 merged 7 commits into
PasarGuard:devfrom
parsa222:reality-scanner

Conversation

@parsa222

@parsa222 parsa222 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a REALITY target scanner: a "Scan target" button in the Xray inbound
editor (next to a REALITY dest/target field) that probes a candidate
handshake target and reports whether it is a good REALITY decoy, plus a
POST /api/core/reality-scan endpoint behind it.

For a given host it checks:

  • TLS 1.3
  • HTTP/2 (ALPN)
  • Key-exchange group (X25519, and post-quantum X25519MLKEM768)
  • HTTP/3 advertisement (Alt-Svc)
  • Certificate validity, issuer/subject, expiry, and the non-wildcard SANs
    (surfaced as suggested valid SNIs)
  • Handshake latency

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:

  1. Main TLS probe via Python ssl (TLS version, ALPN/H2, certificate parsed
    with cryptography).
  2. A self-contained raw TLS 1.3 ClientHello/ServerHello parser to detect the
    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.
  3. Best-effort HTTP/3 detection from the server's Alt-Svc header.

Security

The endpoint could otherwise be an SSRF primitive, so:

  • Every target is resolved and pinned to a public IP. Private, loopback,
    link-local, reserved and cloud-metadata addresses are rejected.
  • Connections are pinned to the resolved IP and never re-resolved, which closes
    the DNS-rebinding hole.
  • DNS resolution is time-bounded and concurrent scans are capped by a semaphore.
  • The endpoint is gated behind the cores:read permission.

API

POST /api/core/reality-scan -> RealityScanResult
Request: { target, sni?, timeout? } (timeout clamped to 1-20s, default 10).

Tests

Backend unit tests in tests/test_reality_scan_unit.py cover 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

  • i18n: no locale keys are included. All new UI strings use
    t(key, { defaultValue: '...' }), so they render in English and fall back
    cleanly with translations missing. Adding fa/zh/ru entries under
    coreEditor.realityScan.* (and coreConfigModal.scanRealityTarget) is a
    good follow-up.
  • The dashboard uses a small hand-written client (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

  • New Features
    • Added REALITY target scanning via a new Core API endpoint (/api/core/reality-scan), returning structured TLS/HTTP/feasibility and certificate metadata.
    • Introduced a “Scan target” action in the Xray editor with multi-target tokenized scanning, bounded parallel execution, per-target status tracking, result summaries, and expandable details.
    • Supports optional SNI override, configurable timeout, and scan cancellation/abort.
  • Tests
    • Added comprehensive unit tests for target parsing, DNS/public-IP resolution, TLS handshake/client-hello parsing, certificate extraction, feasibility logic, concurrency limits, and an optional live network integration test.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

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

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1937e6fe-a327-43ec-944e-0856fc86d586

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

Walkthrough

Adds 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.

Changes

Reality scan workflow

Layer / File(s) Summary
Request, response, and API contracts
app/models/reality_scan.py, app/operation/core.py, app/routers/core.py, dashboard/src/service/reality-scan.ts
Defines validated request/result shapes and connects the authenticated POST endpoint to the frontend service.
Target parsing and public address resolution
app/utils/reality_scan.py, tests/test_reality_scan_unit.py
Normalizes targets, handles SNI and ports, filters non-public addresses, applies DNS timeouts, and tests resolution behavior.
TLS, certificate, group, and HTTP/3 probes
app/utils/reality_scan.py, tests/test_reality_scan_unit.py
Performs TLS and certificate probing, parses custom handshake responses, detects key exchange groups, checks HTTP/3 advertisements, and validates low-level parsing.
Scan orchestration and concurrency
app/utils/reality_scan.py, tests/test_reality_scan_unit.py
Combines probe results into feasibility status, short-circuits unreachable targets, bounds concurrency, and tests orchestration outcomes.
Editor dialog and REALITY target integration
dashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsx, dashboard/src/features/core-editor/components/xray/xray-inbounds-section.tsx
Adds the scan action, optional initial SNI, cancellable request flow, loading and error states, and detailed result rendering.

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
Loading

Poem

I’m a rabbit with packets tucked under my hat,
Scanning TLS where the wild servers sat.
Curves hop, certs glow, and ALPN sings,
HTTP/3 flutters on Alt-Svc wings.
The REALITY field now knows where to leap.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a REALITY target scanner.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Comment thread app/utils/reality_scan.py Fixed
Comment thread app/utils/reality_scan.py Fixed
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

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.

@ImMohammad20000

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9e799 and 17381e7.

📒 Files selected for processing (8)
  • app/models/reality_scan.py
  • app/operation/core.py
  • app/routers/core.py
  • app/utils/reality_scan.py
  • dashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsx
  • dashboard/src/features/core-editor/components/xray/xray-inbounds-section.tsx
  • dashboard/src/service/reality-scan.ts
  • tests/test_reality_scan_unit.py

Comment thread app/utils/reality_scan.py Outdated
Comment thread dashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsx Outdated
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

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.
@ImMohammad20000

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

parsa added 3 commits July 13, 2026 07:22
- _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.
@parsa222

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9e799 and 3ce1f21.

📒 Files selected for processing (8)
  • app/models/reality_scan.py
  • app/operation/core.py
  • app/routers/core.py
  • app/utils/reality_scan.py
  • dashboard/src/features/core-editor/components/xray/reality-scan-dialog.tsx
  • dashboard/src/features/core-editor/components/xray/xray-inbounds-section.tsx
  • dashboard/src/service/reality-scan.ts
  • tests/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.

@parsa222 parsa222 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

next commit

@ImMohammad20000

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

parsa and others added 2 commits July 14, 2026 22:19
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.
@ImMohammad20000
ImMohammad20000 merged commit 9a7022d into PasarGuard:dev Jul 15, 2026
10 checks passed
@Kiya6955

Copy link
Copy Markdown

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

@parsa222

parsa222 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

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".

1

resolved :
2

and you can check it yourself, same ip different sni: (get an api key or use the owner user/pass)

POST /api/core/reality-scan  {"target":"1.0.0.1:443","sni":"www.google.com","timeout":10}

which returns:
cert_valid=false, reason: Hostname mismatch, certificate is not valid for 'www.google.com'

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.

another example:
3

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.
(pq check might have been an overkill but why not....)
https://xtls.github.io/en/config/transports/reality.html#realityobject

4

ImMohammad20000 pushed a commit that referenced this pull request Jul 16, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants