Skip to content

Reality scanner: target-derived SNI + hardening (follow-up to #695) - #700

Merged
ImMohammad20000 merged 4 commits into
PasarGuard:devfrom
parsa222:reality-scan-target-sni
Jul 16, 2026
Merged

Reality scanner: target-derived SNI + hardening (follow-up to #695)#700
ImMohammad20000 merged 4 commits into
PasarGuard:devfrom
parsa222:reality-scan-target-sni

Conversation

@parsa222

@parsa222 parsa222 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #695, which added the REALITY target scanner. This reworks it so the
SNI is derived purely from the target, adds bare-IP domain discovery, and hardens
the scan engine.

What changed

New flow

  • Hostname target (www.microsoft.com:443): resolved to an IP, the hostname is
    sent as the SNI, and the cert is validated against it.
  • Bare IP target (1.1.1.1:443): no SNI is sent the scanner reads the cert the
    IP presents, discovers the domain from it (CN / first non-wildcard SAN), re-validates
    against that, and returns sni_discovered: true with the discovered name.
  • The inbound editor's "Scan target" button now just seeds the dest as the target (it no
    longer pulls serverNames). Multiple targets can be scanned at once; each derives its
    own SNI. A target is suitable when: TLS 1.3 + HTTP/2 (ALPN) + an X25519-family key
    exchange + a valid certificate.

APIPOST /api/core/reality-scan (auth: cores:read)

  • Request: { "target": "host[:port]", "timeout": 1-20 } (the separate sni field was
    removed; it's derived from the target).
  • Response adds sni_discovered: bool; otherwise unchanged (target, host, ip, port, sni, feasible, tls13, tls_version, h2, alpn, x25519, post_quantum, curve, h3, cert_valid, cert_subject, cert_issuer, not_after, server_names[], latency_ms, reason).

Hardening

  • TLS handshake now runs under an absolute wall-clock deadline (non-blocking socket +
    select), so a slow-drip peer can't keep do_handshake() running forever; scans run on a
    dedicated ThreadPoolExecutor so a lingering scan thread can't starve the event loop's
    shared pool (DNS / other to_thread).
  • Concurrency semaphore is keyed per running event loop (WeakKeyDictionary), removing the
    cross-loop RuntimeError.
  • Error paths: cert reason preserved on fallback failure; over-long DNS labels handled
    gracefully (a reason, not a 502); discovered SNI gated through a hostname check.

Tests: 70 unit tests (handshake-deadline, per-loop semaphore, discovery, error paths)
plus opt-in live-network tests.

Summary by CodeRabbit

  • New Features
    • Reality scans now determine and use SNI automatically, including when the target is an IP address.
    • Scan results display the SNI value and a “discovered from IP” badge when applicable.
  • Bug Fixes
    • Removed ability to manually override SNI; scans now rely on detected SNI consistently.
    • Improved TLS/HTTP/3 probing robustness and timeout handling.
  • Localization
    • Added SNI labels and “discovered from IP” text to English, Persian, Russian, and Chinese.
  • Tests
    • Expanded unit and live test coverage for SNI discovery and probing behavior.

parsa added 2 commits July 15, 2026 14:26
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.
…emaphore)

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

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The REALITY scanner removes caller-provided SNI overrides, derives SNI from target hosts or certificates, performs deadline-based TLS probing, exposes discovery status in results, and displays discovered SNI in the dashboard with localized labels.

Changes

REALITY SNI discovery

Layer / File(s) Summary
Scan contracts and target parsing
app/models/reality_scan.py, app/utils/reality_scan.py, app/operation/core.py, dashboard/src/service/reality-scan.ts
Requests no longer include SNI overrides; results include sni_discovered; target parsing derives SNI from hostnames.
Deadline-based TLS probing
app/utils/reality_scan.py, tests/test_reality_scan_unit.py
Non-blocking TLS handshakes, hostname selection, certificate fallback, revalidation, and encoding-error handling were added and tested.
Scan orchestration and result assembly
app/utils/reality_scan.py, tests/test_reality_scan_unit.py
HTTP/3 probing, bounded executor usage, per-loop semaphores, discovered-SNI propagation, and feasibility calculation were updated.
Dashboard scan flow and result display
dashboard/src/features/core-editor/components/xray/..., dashboard/src/service/reality-scan.ts, dashboard/public/statics/locales/*
The UI stops passing initial SNI values and displays discovered SNI information with localized text.
Scanner behavior validation
tests/test_reality_scan_unit.py
Tests cover certificate-name selection, TLS handshake behavior, concurrency primitives, SNI propagation, and feasibility reasons.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RealityScanDialog
  participant scanRealityTarget
  participant _scan_sync
  participant _tls_probe
  participant Certificate
  RealityScanDialog->>scanRealityTarget: submit target and timeout
  scanRealityTarget->>_scan_sync: run scan in bounded executor
  _scan_sync->>_tls_probe: probe target TLS
  _tls_probe->>Certificate: discover usable SNI when needed
  Certificate-->>_tls_probe: return hostname
  _tls_probe-->>_scan_sync: return TLS and SNI state
  _scan_sync-->>scanRealityTarget: return scan result
  scanRealityTarget-->>RealityScanDialog: return result
Loading

Possibly related PRs

  • PasarGuard/panel#695: Earlier REALITY scanner implementation across the same backend, model, and UI flow.

Suggested reviewers: x0sina

Poem

I’m a bunny sniffing TLS,
Finding names where bare IPs rest.
Certificates reveal the way,
While SNI discoveries hop all day.
The dashboard glows with labels bright—
Localized carrots left and right!

🚥 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 reflects the main change: deriving SNI from the target plus scanner hardening.
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.

@parsa222

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 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: 2

🤖 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 271-281: Update the certificate identity extraction logic to
inspect DNS names from the SUBJECT_ALTERNATIVE_NAME extension before evaluating
the subject CN. Return the first valid non-wildcard hostname from the SANs, and
only fall back to the existing CN validation when no usable DNS SAN identity
exists; adjust tests in the related unit test module to cover SAN precedence and
CN fallback.
- Around line 674-675: Update the feasibility calculation in _group_probe to
require an explicitly confirmed X25519-family group, rather than treating
unknown x25519, post_quantum, and curve values as acceptable. Ensure
result["feasible"] is false when group probing leaves these fields unknown,
while preserving the existing TLS 1.3, HTTP/2, and certificate validity
requirements.
🪄 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: f6dd5340-f567-42ab-b0f8-aad310901c4f

📥 Commits

Reviewing files that changed from the base of the PR and between 9a7022d and d4c8f04.

📒 Files selected for processing (11)
  • app/models/reality_scan.py
  • app/operation/core.py
  • app/utils/reality_scan.py
  • dashboard/public/statics/locales/en.json
  • dashboard/public/statics/locales/fa.json
  • dashboard/public/statics/locales/ru.json
  • dashboard/public/statics/locales/zh.json
  • 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 app/utils/reality_scan.py Outdated
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.
@parsa222

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 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.

🧹 Nitpick comments (1)
tests/test_reality_scan_unit.py (1)

211-225: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate certificate generation boilerplate.

The certificate creation logic here is nearly identical to _self_signed_der (defined around line 170). You can avoid this duplication by updating the helper to conditionally add the SAN extension, which keeps the tests focused and easier to maintain.

♻️ Proposed refactor

Update this test to use the helper:

-    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
-    name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "cn-only.example.com")])
-    now = datetime.datetime.now(datetime.timezone.utc)
-    cert = (
-        x509.CertificateBuilder()
-        .subject_name(name)
-        .issuer_name(name)
-        .public_key(key.public_key())
-        .serial_number(x509.random_serial_number())
-        .not_valid_before(now - datetime.timedelta(days=1))
-        .not_valid_after(now + datetime.timedelta(days=90))
-        .sign(key, hashes.SHA256())
-    )
-    assert rs._first_usable_name(cert.public_bytes(serialization.Encoding.DER)) == "cn-only.example.com"
+    der = _self_signed_der("cn-only.example.com", [])
+    assert rs._first_usable_name(der) == "cn-only.example.com"

And update _self_signed_der (around line 170) to support an empty sans list since cryptography enforces a non-empty list for SubjectAlternativeName:

def _self_signed_der(cn: str, sans: list[str]) -> bytes:
    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    name = x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, cn), 
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Test Org")
    ])
    now = datetime.datetime.now(datetime.timezone.utc)
    
    builder = (
        x509.CertificateBuilder()
        .subject_name(name)
        .issuer_name(name)
        .public_key(key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(now - datetime.timedelta(days=1))
        .not_valid_after(now + datetime.timedelta(days=90))
    )
    
    if sans:
        builder = builder.add_extension(
            x509.SubjectAlternativeName([x509.DNSName(s) for s in sans]), 
            critical=False
        )
        
    cert = builder.sign(key, hashes.SHA256())
    return cert.public_bytes(serialization.Encoding.DER)
🤖 Prompt for 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.

In `@tests/test_reality_scan_unit.py` around lines 211 - 225, Update
_self_signed_der to accept an empty sans list and add the SubjectAlternativeName
extension only when sans is non-empty; then replace the duplicated
certificate-building logic in
test_first_usable_name_falls_back_to_cn_without_san with a call to
_self_signed_der using the CN and an empty SAN list.
🤖 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.

Nitpick comments:
In `@tests/test_reality_scan_unit.py`:
- Around line 211-225: Update _self_signed_der to accept an empty sans list and
add the SubjectAlternativeName extension only when sans is non-empty; then
replace the duplicated certificate-building logic in
test_first_usable_name_falls_back_to_cn_without_san with a call to
_self_signed_der using the CN and an empty SAN list.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05c08913-8b0c-4ac0-a806-8f75c26aeead

📥 Commits

Reviewing files that changed from the base of the PR and between d4c8f04 and 8ee5e48.

📒 Files selected for processing (2)
  • app/utils/reality_scan.py
  • tests/test_reality_scan_unit.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/utils/reality_scan.py

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 PasarGuard#700 review nitpick.
@parsa222

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

fuhhhhh

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

The Comment image shows a "brain expansion" style meme with two panels (a smug face and a determined face reaching toward each other), a classic meme format used to depict escalating power/hype. Overlaid on the two panels are three small images: a chaotic glowing "energy skull/face" icon, a photo of Benedict Cumberbatch as Doctor Strange, and the CodeRabbit rabbit logo in the top-right corner — playfully suggesting CodeRabbit is being "summoned" for a powerful full review.

@parsa222 Got it — kicking off a full review of the entire PR now. 🐇

✅ 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 `@app/utils/reality_scan.py`:
- Around line 253-261: Update _looks_like_hostname in app/utils/reality_scan.py
(lines 253-261) to reject IP literals and validate each label using IDNA/LDH
hostname rules, including allowed characters, hyphen placement, and trailing-dot
handling. Add coverage in tests/test_reality_scan_unit.py (lines 479-483) for
invalid characters, leading or trailing hyphens, repeated trailing dots, and
numeric IP literals; the tests site requires direct additions.
🪄 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: bf90edfb-74bf-4789-a512-30d5b18ee74a

📥 Commits

Reviewing files that changed from the base of the PR and between 9a7022d and c533339.

📒 Files selected for processing (11)
  • app/models/reality_scan.py
  • app/operation/core.py
  • app/utils/reality_scan.py
  • dashboard/public/statics/locales/en.json
  • dashboard/public/statics/locales/fa.json
  • dashboard/public/statics/locales/ru.json
  • dashboard/public/statics/locales/zh.json
  • 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
@ImMohammad20000
ImMohammad20000 merged commit f1a30af into PasarGuard:dev Jul 16, 2026
10 checks passed
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.

2 participants