CloudflareInterceptor: Attempt to solve challenge when interactive - #3858
Open
tigeryu8900 wants to merge 15 commits into
Open
CloudflareInterceptor: Attempt to solve challenge when interactive#3858tigeryu8900 wants to merge 15 commits into
tigeryu8900 wants to merge 15 commits into
Conversation
unseensnick
added a commit
to unseensnick/Reikai
that referenced
this pull request
Aug 27, 2026
The interactive Cloudflare solver now tabs onto the verification box and hits space, instead of tapping a point it worked out from the widget's rectangle. Same speed, and it no longer has to guess where the box is. - Twenty-nine interactive challenges over six VPN rounds on the Fold cleared on keys alone, 2.2 to 3.1 seconds each, with four or five hosts in parallel finishing in 2.9 to 3.5 seconds. - The four rounds run before the tap came out still carried it as a fallback and never once reached it. Under the hood: - Deletes the checkbox inset, the widget-rect walk in the page probe and the viewport scaling. That arithmetic was the part most exposed to Cloudflare restyling its widget. - The keys land even with the soft keyboard up, so the IME does not take focus from an attached WebView. Suggested by tigeryu8900, who ported the solver upstream the same way (mihonapp/mihon#3858).
unseensnick
added a commit
to unseensnick/Reikai
that referenced
this pull request
Aug 27, 2026
A challenge Cloudflare has abandoned, and a WebView whose renderer dies, both used to hold the request for the full thirty-second timeout before failing. Each now ends the wait as soon as it happens, so a source that cannot be cleared says so quickly instead of hanging. - A dead renderer no longer takes the app process down with it either, which is what the unhandled case does. - The abandoned-challenge exit is trusted only while the in-app solver is off. Armed, it keeps pressing, because Cloudflare reissues after a failed round often enough to be worth the wait. - The bypass WebView is now pushed off screen to the left rather than the right, so it stays hidden if the window grows under it. Ported from mihonapp/mihon#3858. Fifteen more interactive challenges cleared on the Fold across three VPN rounds with no change in timing; neither new exit fired, since neither can be provoked from a normal run.
unseensnick
added a commit
to unseensnick/Reikai
that referenced
this pull request
Aug 27, 2026
The solver record now says what is actually verified and what upstream is doing with the same idea, so a future sync does not walk into it blind. - Breadth is five hosts on one device over one VPN, across forty-four solves. Reliable on those, unknown beyond them, which is why the switch stays off by default. - Notes that mihonapp/mihon#3858 carries a version of this upstream, and where it will collide on the next sync: the RK island in CloudflareInterceptor, and ForegroundActivity.kt at the same path.
unseensnick
added a commit
to unseensnick/Reikai
that referenced
this pull request
Aug 28, 2026
The interactive Cloudflare solver used to give up whenever the app had no window on screen, which is exactly when a background library update runs. It now falls back to a script inside the challenge frame that ticks the box for itself, so an update no longer stalls on a challenged source. - A library update behind an interactive challenge now gets through on its own, where before it failed and left the source unchecked. - Nothing changes while you are using the app: that path still uses real key presses, which stay about four times faster. - A fallback that cannot land now fails in twenty seconds instead of sitting out the full thirty second wait. Under the hood: - TurnstileSolver.attach picks a press mechanism instead of refusing to arm; the window setup moved into attachToWindow. - The in-frame script is borrowed from mihonapp/mihon#3858 at e6de3a7a1, verbatim apart from three stripped console.log lines, and is scoped to challenges.cloudflare.com so it never runs in the source's own page. - Arming suppresses the upstream interactive and fail aborts, so the fallback owns giving up; onGiveUp restores that. - The budget timer is a main-looper Handler, not View.postDelayed, which never fires on a detached view. Measured on the Fold: three real update solves at 9.4 to 12.2s, and the foreground path unchanged at 2.4 to 3.0s across five hosts. The fallback clears nothing when the app is backgrounded with no foreground service, which is why ForegroundActivity deliberately still reports a paused activity. Full record in docs/dev/plans/turnstile-solver.md.
unseensnick
added a commit
to unseensnick/Reikai
that referenced
this pull request
Aug 28, 2026
Concurrent requests to a challenged host used to queue on the Cloudflare bypass's own map of pending solves; they now take a per-host read/write lock in the interceptor base, and a request that queued behind a solve re-checks the cookie jar instead of retrying blind. No user-visible change: measured back to back against the old path on the same five hosts, every challenged request resolved either way, and solve times were 2.4 to 2.9 seconds before against 2.5 to 3.2 after. Under the hood: - WebViewInterceptor owns the lock, getNonce and isBypassed; intercept takes a nonce and returns null to mean "solved, retry normally". - TurnstileSolver.onlyOncePerHost and its future map are gone. - The base closes the challenge response on the sibling shortcut, since that path returns without reaching the subclass that used to close it. Without it OkHttp refuses the retry and the source renders an error. MARKED FOR POSSIBLE REMOVAL. This is taken from mihonapp/mihon#3858, which is unmerged, and it landed on a benefit that stayed unmeasured: the advantage is on the failure path, where a sibling would otherwise retry into a 403, and that case was never reproduced. If the PR is abandoned or the divergence stops being worth it, remove it by hand rather than with git revert, since work will have landed on top: delete locksByHost and the lock wrapping in WebViewInterceptor.intercept, drop getNonce / isBypassed and the nonce parameter, return Response again, and restore onlyOncePerHost around the resolveWithWebView call. The same recipe and the regression above are in docs/dev/plans/turnstile-solver.md.
Member
|
i'm not sure about the auto solver. it might get us blocked permanently by cloudflare. the other changes seems to be useful. |
Contributor
Author
|
I see. Just to clarify, by the auto solver you mean the simulated key events and the injected JavaScript solver, or just the JavaScript solver? |
Member
|
both |
tigeryu8900
force-pushed
the
cloudflare-solve-interactive
branch
from
August 30, 2026 09:45
7701841 to
0a1f07d
Compare
Contributor
Author
|
Got it. I removed the solver. |
unseensnick
added a commit
to unseensnick/Reikai
that referenced
this pull request
Sep 1, 2026
The half of the interactive Cloudflare solver that runs with no screen open now has its own switch, off by default, so wanting global search to work no longer means accepting a script that runs inside the challenge frame. The borrowed script itself gained strict mode and two fixes. Solver: - A new "Solve with the app in the background" switch sits under the existing one and starts off. With it off and no screen open the solver does not arm at all, so the request behaves the way it did before the in-frame path existed. - Key presses are spaced by a random 70 to 160ms instead of a flat tenth of a second, so solves running at once no longer share one cadence. Under the hood: - The borrowed in-frame script runs in strict mode, which closes the caller-chain read its own author flagged. Upstream added it after Reikai took the script (mihonapp/mihon#3858). - removeEventListener now maps the caller's listener forward to the wrapper that was actually registered. It read the reverse map, so removal never matched: removed listeners kept firing and the wrappers leaked. Measured against a control page. - The Gecko branch builds its WeakMap with new. Unreachable on Android, since Blink always takes the V8 branch, but it would have thrown. - Dropped the dead Object.definePropery call and recorded why the walk must never redefine the prototypes: the proxy registrations are its whole product, and redefining anonymises 48 members of MouseEvent, UIEvent and Event without changing anything a listener can observe. Verified on device: key path 2.2s to clear, the gate declining to arm with no window, and the in-frame path injecting into 4 of 4 interactive hosts and clearing 3 of them in 12.2 to 13.5 seconds, inside the pre-hardening band. 1262 app and 75 domain tests pass.
unseensnick
added a commit
to unseensnick/Reikai
that referenced
this pull request
Sep 1, 2026
The solver asked androidx for an isolated world before checking whether the WebView had one, and that call throws when it does not. The throw was caught, logged and then reported as a successful arm, which suppressed the caller's own aborts while nothing was left to release the wait: every challenged request on such a WebView cost its whole 30 second timeout. Invisible on a current WebView, which is why it shipped. - The isolated world joins the other two features the solver requires, so a WebView without one declines cleanly and the switch stays off the settings screen rather than offering something that cannot work. - Arming failure now reports not armed, leaving the caller in charge. - The page-world fallback branch is deleted rather than made reachable. It could never run, and a probe in the page's own world is the measured dead end that makes Cloudflare reissue the challenge. - A failed solve deletes cf_clearance. Cloudflare issues one on a round it refused, so leaving it behind made a sibling queued on that host skip its own solve and retry into a 403. Taken from mihonapp/mihon#3858. - The probe is scoped to the page being solved instead of every frame on it, including third party ones whose reports were discarded anyway. Verified on device: four hosts cleared in 2.2 to 3.2 seconds, which is only reachable if the scoped probe still reports, and re-running without clearing anything challenged nothing, so a solve keeps its clearance. 1262 app and 75 domain tests pass.
unseensnick
added a commit
to unseensnick/Reikai
that referenced
this pull request
Sep 1, 2026
The solver used to switch itself off entirely, with the setting greyed out and no explanation, on any WebView older than a feature that shipped in March. It now solves there too, on Cloudflare's own events, and the setting is always available. - Interactive challenges are solved on older WebViews instead of the feature quietly not existing there. Under the hood: - The feature it needed is negotiated against the installed WebView, so the gate was recency, not Android version. The reason recorded for refusing covered the page probe, not the events: a poll in the page's own world is what makes Cloudflare reissue, while the two events a solve turns on already reach the interceptor's bridge on every challenged page. Without a world the solve reads those, adds nothing new to the page, and loses only the markup fallback. - The clearance lands with the navigation after complete, not with the event, so a solve with no probe asks again every 250ms for five seconds. Without that it spent its whole budget before the post-latch jar check rescued it, which is what the first build did. - The probe no longer ships Cloudflare's other events across the bridge. They were collected, serialised and dropped unread; the interceptor's own bridge already logs all of them. - isSupported is gone, since it could only answer true; canWatch says whether there is a world to poll from, and the harness asks that. Borrowed from mihonapp/mihon#3858, which degrades where this declined. Device-verified on the Fold across three fresh exits: with the branch forced, three hosts accepted 252 to 261ms after complete; on the normal path four hosts accepted 165 to 802ms; and a background library update with no activity solved headless. Tests: 1262 app, 75 domain.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When an interactive challenge is detected, attempt to solve the challenge instead of aborting by attaching the WebView to the current view and sending the Tab and Space keys.
I also added a locking mechanism to make sure that only one request per host is solving the challenge at a time.
I took inspiration from unseensnick/Reikai@14d3d54.
This is the overall logic of the solver:
chain.proceed(request). If the response is successful, release the read lock and return the response. Otherwise, get thecf_clearancecookie before releasing the read lock.chain.proceed(request).interactiveBeginevent, if the fallback solver isn't injected, attach the WebView to the view group of the current activity and simulate Tab and Space keys. If the view group is unavailable or keys fail to send, inject the fallback solver and reload the page. Abort if thecompleteevent doesn't happen within five seconds.chain.proceed(request).