sing-vis
A web app that explains how a sing-box configuration routes a domain or IP. Paste a sing-box JSON config, enter a list of domains/IPs, and sing-vis shows — for each one — which DNS rule and route rule it hits, every condition evaluated along the way, and the final DNS server and outbound.
It runs entirely in your browser: the matching engine is sing-box's own Go code compiled to WebAssembly. There is no backend, nothing is uploaded, and you can host it as static files (including on GitHub Pages).
Quick start
Requires Go (the module targets go 1.24.7; the default GOTOOLCHAIN=auto fetches a matching toolchain automatically). No Node/npm — the frontend has no build step.
./run.sh # builds the wasm engine, serves http://127.0.0.1:8787
./run.sh 9000 # custom port
./run.sh 0.0.0.0:9000 # custom host:port (expose on your LAN)
Open the printed URL. That's it.
Using it
-
Paste a config. The editor opens pre-filled with a sample. Replace it with your own sing-box JSON in the config box (JSONC comments are fine), and give the profile a name.
-
List what to check. In Domains / IPs to check, put one host per line — domains (
www.google.com) or raw IPs (1.1.1.1). Lines starting with#are ignored. Pasted URLs andhost:portstrings are accepted (scheme/path/port are stripped). -
Click ▶ Analyze. The first run downloads the ~3.3 MB wasm engine (cached afterwards). For every input you get:
- DNS routing — which
dns.rulesrule matches, the resulting DNS server (orreject/other action), and the outbound detour that server is reached through. - Route matching — every
route.rulesrule in order, each condition's result (match / no match /UNKNOWN?), whichrule_setmatched and on which headless rule, down to the final outbound. - Resolved IPs — the A/AAAA records fetched via DoH (shown when a domain is resolved).
Click any result card to expand it, and any rule step to see its per-condition breakdown.
- DNS routing — which
-
Save the profile with 💾. Profiles live in your browser (IndexedDB), persist across reloads, and appear in the left sidebar. Settings persist too. Everything stays on your machine.
Toolbar options
- Resolve IPs for IP rules (on by default) — pre-resolves each domain via DoH so
ip_cidrand IP rule-set rules match the resolved address, matching what you'd intuitively expect. Turn it off for strict sing-box semantics, where IP rules only match after an explicitresolveaction. - network: any / tcp / udp — an assumed connection network, so rules filtering on
networkcan be evaluated. - ⚙ Settings — the DoH endpoint (default
https://1.1.1.1/dns-query).
Conditions that can't be known offline
Some rule conditions depend on live connection attributes that don't exist for a "what would this domain do?" query — protocol, process_name, inbound, clash_mode, source address/port, destination port, etc. These show as UNKNOWN? (amber). If such an undeterminable terminal rule sits before the definite match, the outcome is flagged "depends on assumptions", because at runtime that rule could preempt the result.
Rule sets
- inline — read straight from the config.
- remote — fetched live from its
url(source.jsonor binary.srs, auto-detected). The URL must allow CORS (GitHub raw does). - local — the browser can't read disk paths, so upload the file under Local rule-set files in the editor, keyed by the rule-set tag or its path.
.srsis read as binary; anything else as source JSON.
Why it's faithful
Rather than re-implement sing-box's matching, sing-vis imports sing-box's own Go packages for the version-sensitive parts:
option— parses the config with sing-box's real unmarshalers (rule/action/rule-set dispatch, JSONC).common/srs— reads the binary.srsformat (compiled domain succinct-sets and IP sets).sing/common/domain— the actual succinct-set domain/suffix matcher (the same coderoute/rule.DomainItemwraps).
sing-vis owns only the orchestration — AND across fields / OR within a field's array / logical and·or / invert, first-terminal-match-wins, the resolve → ip_cidr lifecycle, and the final fallback — so it can instrument every step and report exactly which condition and rule-set matched.
The engine deliberately does not import sing-box's
adapter/route/rulepackages: they pull in the full outbound/dialer/sing-tun tree, which doesn't compile forwasm. The only rule fields needing a connection context aredomain/network/query_type— the domain matcher is called directly onsing/common/domain(identical toroute/rule.DomainItem), and network / query_type are plain membership tests. Seeinternal/engine/conditions.go.
Building & hosting
./run.sh is just ./build.sh followed by a static file server. To build and serve separately:
./build.sh # -> web/singvis.wasm (+ .gz) and web/wasm_exec.js
python3 -m http.server -d web 8787 # or any static server
After build.sh, the web/ directory is completely self-contained. To publish on GitHub Pages, serve web/ (commit it, or copy it to a gh-pages branch / docs/ folder). The built web/singvis.wasm, web/singvis.wasm.gz, and web/wasm_exec.js are generated artifacts (gitignored); regenerate them any time with ./build.sh.
The wasm build overlay
Three files in the sing dependency (common/buf/buffer_unix.go, common/bufio/vectorised_unix.go, common/bufio/copy_direct_posix.go) reference golang.org/x/sys/unix, which has no GOARCH=wasm equivalent. Those code paths (raw-socket readv/writev) never run in a browser. build.sh swaps them for wasm-safe stubs in wasmbuild/_stubs/ via go build -overlay (generated by wasmbuild/gen-overlay.sh). Only the wasm build uses the overlay — native builds and go test are unaffected.
Testing
The engine is platform-agnostic and tested natively (no wasm needed):
go test ./internal/engine
CORS
DoH resolution and remote rule-set fetches originate from the browser, so those endpoints must send Access-Control-Allow-Origin. The defaults do — Cloudflare/Google DoH JSON (https://1.1.1.1/dns-query, https://dns.google/dns-query) and raw.githubusercontent.com. Point sing-vis at an endpoint that doesn't, and that item shows a fetch error; swap it for a CORS-enabled one, or upload the rule-set file locally. The resolver uses the DoH JSON API (?name=&type=&ct=application/dns-json), a CORS "simple request" that avoids a preflight.
Project layout
cmd/wasm/ js/wasm entry point: exposes singvisAnalyze() to JS
internal/engine/ the matching engine (platform-agnostic, unit-tested)
parse.go config → option structs (route/dns rules, rule sets, servers)
conditions.go per-condition tri-state evaluation (match/no_match/unknown)
rules.go field extraction + logical-rule recursion
ruleset.go inline/remote/local rule-set loading (incl. .srs binary) + eval
route.go route-rule orchestration (actions, resolve, final)
dns.go dns-rule orchestration (actions, dns.final, server → detour)
analyze.go top-level per-input driver
internal/dnsx/ DoH (JSON API) resolver
web/ static single-page frontend (no build step)
index.html markup
app.js UI + rendering
storage.js profiles & settings in IndexedDB
worker.js Web Worker: loads the wasm engine, runs analyze off the UI thread
wasmbuild/ wasm build support (overlay generator + unix→wasm stubs)
sing-box/ upstream sing-box clone (imported via a go.mod replace)
