2026-07-24 13:42:10 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:42:10 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00
2026-07-24 13:29:16 +00:00

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

overview

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

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

  2. 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 and host:port strings are accepted (scheme/path/port are stripped).

  3. Click ▶ Analyze. The first run downloads the ~3.3 MB wasm engine (cached afterwards). For every input you get:

    • DNS routing — which dns.rules rule matches, the resulting DNS server (or reject/other action), and the outbound detour that server is reached through.
    • Route matching — every route.rules rule in order, each condition's result (match / no match / UNKNOWN?), which rule_set matched 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.

  4. 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_cidr and 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 explicit resolve action.
  • network: any / tcp / udp — an assumed connection network, so rules filtering on network can 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 .json or 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. .srs is 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 .srs format (compiled domain succinct-sets and IP sets).
  • sing/common/domain — the actual succinct-set domain/suffix matcher (the same code route/rule.DomainItem wraps).

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/rule packages: they pull in the full outbound/dialer/sing-tun tree, which doesn't compile for wasm. The only rule fields needing a connection context are domain / network / query_type — the domain matcher is called directly on sing/common/domain (identical to route/rule.DomainItem), and network / query_type are plain membership tests. See internal/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)
S
Description
No description provided
Readme
230 KiB
Languages
Go 58.7%
JavaScript 25.6%
CSS 11.2%
Shell 3.4%
HTML 1.1%