Repository navigation
Conversation
A new page (Clean IP scanner) tests random addresses from Cloudflare's IPv4 ranges from the browser of whoever opens it, the only place that sees the user's own ISP (the panel's server is usually abroad). Each probe is a WebSocket to wss://<ip>/: the panel's CSP already allows wss: to any host, and a fast TLS failure means the IP answers while silence means it is blocked. Checked against a real TLS client from an Iranian ISP: no false positives in two runs of 75 IPs. GET /panel/api/cleanip/network names the caller's ISP (AS number via ipwho.is, ip-api.com as fallback, cached for an hour, through the panel's egress proxy) with the short names Iranian panels use (MCI, MTN, MKH, ...). The best IPs go into a host group "<group> · <ISP>" that copies the chosen group's settings, so each ISP gets its own configs; saving again replaces that ISP's IPs, and the page lists and deletes the ISP groups.
…anner A browser can neither upload through an IP nor cut the TLS ClientHello, and some ISPs pass the handshake but cut the connection after ~1.3 KB, or block the SNI unless the hello is fragmented. The scanner page now makes a one-day command (curl ... | python3 - / irm ... | python -) that runs a standard-library script on a device on the ISP to test: TLS with the CDN domain as SNI and a real upload through each Cloudflare IP, then 26 Xray fragment settings (tlshello and TCP segments, by length and delay) against no fragment on five IPs. It posts the result back with the token alone. The page shows each report, and saving writes the chosen IPs to the ISP's host group "<group> · <ISP>" with the chosen fragment as a tcp "fragment" Final Mask (lengths/delays arrays, other masks kept), so the JSON subscription and the links' fm= give it to that ISP's clients. "No fragment" is preselected unless a setting passes more often. POST /panel/api/cleanip/command and GET /panel/api/cleanip/command/:token sit behind the session; GET <base>cleanip/run/:token/script.py and POST .../report are public, keyed by an unguessable 24-hour token that can only add a report to its own job. Reported rows are validated (IPv4, fragment packets/ranges, colo) before they are kept.
… on WARP/VPN The page now follows the order the work is done in: 1 your network (ISP), 2 CDN domain (host group, port, the SNI the tests use, editable), 3 fragment and upload test (the one-day command and the fragment to keep), 4 quick scan (browser), 5 save (a summary of group, IPs and fragment, then save). The scan table lists the upload-tested IPs first, marked, then the quick ones. A visitor seen from a Cloudflare network (WARP, a VPN on Cloudflare) gets a red warning instead of an ISP, so a scan is not saved for the wrong network. A panel behind Cloudflare's proxy reads CF-Connecting-IP, but only when the request comes from Cloudflare's published ranges, so the header can not be spoofed by a direct client.
…the domain (error 1034) Every Cloudflare edge answers the TLS handshake, but on an IP outside the zone's allowed ranges (and on third-party IPs in front of Cloudflare) it replies 403 "error code: 1034", so configs on it never connect. The browser scan could not see that and the full test counted any HTTP reply as a pass. POST /panel/api/cleanip/verify now checks found IPs from the panel's server with a real HTTPS request for the domain (ok / restricted / unreachable); the page runs it after a scan, keeps restricted IPs out of the selection, and can check and remove restricted IPs already saved in a host group and its ISP groups. The full-test script also rejects Cloudflare's 1xxx error pages.
…le into common.tsx The DNS tunnel page (a separate PR) shows the same ISP card and numbered steps; keeping them in one module lets both pages share them without importing a page component.
…ith a country filter The upload test already reports the CF-RAY colo of every working IP, but the page dropped it. Users pick IPs by where they exit (a German edge for a German server), so the table now names the city and country in the panel language and can be filtered by country.
…valid rows A report whose IPs and fragments were all rejected marshalled them as null, and the page crashed on report.fragments.find.
…s, download CSV The scanner only sampled Cloudflare's ranges. Admins often have IPs or ranges that worked before, and edges in the /24 of a working IP tend to work too, so the quick scan now takes a pasted list (IPs or CIDRs) and has "Find more nearby". Results download as CSV with the datacenter city and country.
Long step titles were cut off and the Copy / CSV buttons ran into them on narrow screens: titles now wrap and the buttons drop their text on mobile.
The scanner's strings were English outside en-US and fa-IR.
…uick scan's hits The scanner only knew Cloudflare's ranges. The host group's CDN is now picked in the domain step (ranges served by GET /panel/api/cleanip/cdns, the same defaults as CF-Clean-Scanner) and both tests sample it. The browser can not read an edge's datacenter, so the command now upload-tests every quick-scan hit (up to 100, was 5 for the fragment test only): they get a datacenter, or show upload failed. The script reads the datacenter header of each CDN and prints why IPs failed (timeout, certificate, reset, ...) with a hint, and falls back to certifi where Python has no CA store.
The scanner sampled every default range, including popular extra ranges (5.x and others) that fail on many ISPs, with no way to leave them out. Step 2 now lists each CDN's range groups (official on, whole-AS groups opt-in) with a checkbox per group and per range plus extra CIDRs; the choice is kept per CDN and both the quick scan and the command sample exactly it.
… the panel's domain
On MCI the one-day command died in curl ("unexpected eof"): the ISP cuts the
panel's own domain, so neither the script download nor the report could reach
it. The command response now also carries curl --resolve variants that pin the
panel's domain to the fastest IP the quick scan found, and the script fetched
with ?via= sends its report through that same IP. Offered only when the panel
is opened by a domain and a page IP exists; it works when the panel's domain is
behind the same CDN, which the hint says.
|
Verdict: Fits with gaps (feature). Operators of CDN-fronted inbounds in filtered networks currently use outside scanners to find edge IPs that work on each ISP, then copy them into Hosts by hand. This pull request adds a Clean IP page that does that work inside the panel. It runs a quick browser scan, a server-side check for Cloudflare's "error 1034" edges, and an optional one-day script that a device on the target ISP runs for an upload and fragment test. The results are saved into a per-ISP copy of an existing Host group. The new routes, schema allowlist and docs copy are all in place, and nothing touches the database, Xray config generation or node sync. The main gap is the ISP label: a full-test report from a phone on one ISP is saved under whatever ISP the admin's browser is on. The commit that adds the "via a clean IP" command is written for ISPs that block the panel's domain, and in that case the browser and the test device are on different networks. A Host group whose path carries a query string, such as an early-data No blocking issues — 0 HIGH / 2 MEDIUM / 4 LOW open (6 new), 0 pre-existing Reviewed head: 151e05d Evidence. There is no linked issue, so the claim comes from the body and the 14 commits. Panel routes sit under the authenticated Design. The one-day command's URL is built from the Host header of the admin's own request ( Findings MEDIUM — MEDIUM — LOW — LOW — LOW — LOW — Coverage (head 151e05d, 46 files, +6,795/−0; the diff was not built or run here)
|
|
The Clean IP scanner is useful, but probing many IPs from a datacenter VPS can be flagged as network/port scanning under hosting providers' acceptable-use policies. That can result in abuse complaints, server suspension/termination, or account restrictions. I reviewed the current code:
Suggested safeguards:
The current server-side probe uses port 443, not a multi-port sweep, but contacting large numbers of destination IPs may still be treated as scanning by the datacenter. @MHSanaei @Plus98ir — please consider these safeguards a merge prerequisite. A panel feature should not put operators' servers or hosting accounts at risk. |
Review on MHSanaei#6950: outgoing connections from the panel's server to many IPs can get it flagged by its hosting provider, and the check took any IPv4, private ones included. The server now only contacts IPs inside the chosen CDN's published ranges (others come back "skipped"), at most 50 per request, 4 at a time and 300 distinct IPs an hour across requests ("limited" past that; an IP checked this hour is answered from memory), and stops when the caller goes away. The page no longer checks every scan hit by itself: the admin checks the selected or saved IPs after a confirmation that names the count and the reason, and can cancel. Also from the review: - a full-test report from another ISP than the browser's is saved under the report's ISP, without the browser's own quick-scan hits - a Host path with a query (/ws?ed=2048) gets the full-test command - the script no longer retries its report with certificate checks off, and no longer crashes before reporting when it has fewer than five IPs - Cloudflare's IPv4 list exists once, for both scanning and CF-Connecting-IP
|
Thanks, all addressed in 3d59e03.
The bot's findings are fixed in the same commit: a full-test report from another ISP is saved under that ISP, a Host path with a query ( |
Review on MHSanaei#6950: outgoing connections from the panel's server to many IPs can get it flagged by its hosting provider, and the check took any IPv4, private ones included. The server now only contacts IPs inside the chosen CDN's published ranges (others come back "skipped"), at most 50 per request, 4 at a time and 300 distinct IPs an hour across requests ("limited" past that; an IP checked this hour is answered from memory), and stops when the caller goes away. The page no longer checks every scan hit by itself: the admin checks the selected or saved IPs after a confirmation that names the count and the reason, and can cancel. Also from the review: - a full-test report from another ISP than the browser's is saved under the report's ISP, without the browser's own quick-scan hits - a Host path with a query (/ws?ed=2048) gets the full-test command - the script no longer retries its report with certificate checks off, and no longer crashes before reporting when it has fewer than five IPs - Cloudflare's IPv4 list exists once, for both scanning and CF-Connecting-IP
…nbound The page only offered existing host groups and always wrote a separate "<host> · <ISP>" group, so an admin without a host had to make a dummy one first and the scanned IPs never landed on the host they picked. CDN-capable inbounds without a host are now listed and get their host created on save, a "Save into" choice writes either into the host itself or its ISP group (default: the ISP group when one exists), the saved host is turned on, and the tested domain becomes the SNI when it has none.
|
Pushed ca5c47f after testing on a fresh server:
Tests: |
The one-day command piped whatever the script URL returned into Python. On a panel reached over plain HTTP that fetch crosses the ISP being tested, so anyone on the path could swap in their own code. The script (and the pinned variant fetched through a page IP) is now fixed when the command is made, and each command is a python -c that fetches it and runs it only if its SHA-256 matches. - The pinned command maps the panel's domain to the page IP for the fetch, as curl --resolve did, so it no longer needs curl and works the same in PowerShell, where a pipe re-encodes the script and would break any hash. - The command's base URL honours X-Forwarded-Proto/-Host only from a trusted proxy, like resolveHost, and keeps the port the browser used.
Brings the clean IP and DNS resolver test fixes from MHSanaei#6950/MHSanaei#6951 to this branch: each one-day command is a python -c that runs its script only if the SHA-256 matches the one fixed when the command was made, and the shared one-day store keeps those scripts. The finder now uses the same trusted-proxy panelBaseURL as the other pages, so the two copies of it become one.
|
Pushed 968f8d0. The review of #7048 found the same problem in this PR's one-day command, so it is fixed here too.
Tests: |
The fake panel's handler runs on the server goroutine while the test resets and reads the fetch list, which the race job flagged on MHSanaei#6950.
…omically The fake panel's handler runs on the server goroutine while the test resets and reads the fetch list, which the race job flagged on MHSanaei#6950.
…omically The fake panel's handler runs on the server goroutine while the test resets and reads the fetch list, which the race job flagged on MHSanaei#6950.
Summary
Adds a Clean IP scanner page (
/clean-ip). It finds CDN edge IPs that work on a given ISP and saves them into a Host group as<group> · <ISP>(for exampleCDN · MCI), so each ISP's subscribers get IPs that work on their network. It supports Cloudflare, Fastly, Gcore and CloudFront.Why
In censored networks (Iran, Russia, Turkmenistan, …) CDN-fronted inbounds only work through "clean" edge IPs. Which IPs are clean differs per ISP and changes over time. Admins currently run external scanners (CF-Clean-Scanner and similar) and copy the results into Hosts by hand. This page does it inside the panel, per ISP, and writes the result where 3x-ui already reads it: Host groups.
How it works (five steps on the page)
CF-Connecting-IPis trusted only when the request really came from Cloudflare.wss://<ip>/(the panel's CSP allowswss:). You pick the CDN and which of its ranges to scan, as in CF-Clean-Scanner. You can also paste your own IP list, find more IPs next to working ones, or download the result as CSV. Each working IP shows its datacenter (city and country) and can be filtered by country.curl … | python3 -, or PowerShell) to run on a device on that ISP.fragmentsettings (tlshello/1-1, length and delay) against no fragment.curl --resolve), and the report goes through that IP too.fragmentfinalMask to that group. A "Check saved IPs" / "Remove restricted" action re-checks rows saved earlier.API (all documented in
endpoints.ts,openapi.jsonand the docs MDX)GET /panel/api/cleanip/networkGET /panel/api/cleanip/cdnsPOST /panel/api/cleanip/commandGET /panel/api/cleanip/command/:tokenPOST /panel/api/cleanip/verifyGET <base>cleanip/run/:token/script.py,POST <base>cleanip/run/:token/reportCommands live in memory only: at most 50, each valid for 24h, and lost on restart. The report endpoint accepts only its own token, caps the body at 256 KB, and validates every row (IPv4 only, known fragment shapes, bounded numbers).
Type of change
Areas affected
How was this tested?
internal/web/service/cleanip_test.go) cover:CF-Connecting-IPtrust rule;null;--resolvecommands.make genis clean, docs MDX regenerated, andgo test ./internal/web/..., typecheck, lint, format and build all pass.Screenshots / recordings
Steps 1-4 after a quick scan (local test panel, example domain):
The one-day full-test command:
Phone width:
Breaking changes
None. It is a new page and new routes. There is no DB migration: the scanner only writes ordinary Host rows through the existing host service.
Checklist
go build ./...and the test suite pass locally.npm run lint,npm run typecheck, andnpm run buildpass.