From 2275bd7dc4418fb90e5123308d38da6810625a25 Mon Sep 17 00:00:00 2001 From: iceBear67 Date: Fri, 24 Jul 2026 13:29:16 +0000 Subject: [PATCH] init --- .gitignore | 5 + README.md | 108 ++++ build.sh | 42 ++ cmd/wasm/main.go | 103 ++++ docs-shot.png | Bin 0 -> 163355 bytes go.mod | 20 + go.sum | 26 + internal/dnsx/doh.go | 169 +++++++ internal/engine/analyze.go | 134 +++++ internal/engine/conditions.go | 473 ++++++++++++++++++ internal/engine/dns.go | 111 +++++ internal/engine/engine.go | 42 ++ internal/engine/engine_test.go | 208 ++++++++ internal/engine/parse.go | 128 +++++ internal/engine/route.go | 185 +++++++ internal/engine/rules.go | 314 ++++++++++++ internal/engine/ruleset.go | 248 +++++++++ internal/engine/types.go | 114 +++++ run.sh | 24 + sing-box | 1 + wasmbuild/_stubs/buf_buffer_unix.go | 11 + wasmbuild/_stubs/bufio_copy_direct_posix.go | 76 +++ wasmbuild/_stubs/bufio_vectorised_unix.go | 32 ++ wasmbuild/gen-overlay.sh | 21 + web/app.js | 526 ++++++++++++++++++++ web/index.html | 42 ++ web/storage.js | 138 +++++ web/style.css | 282 +++++++++++ web/worker.js | 58 +++ 29 files changed, 3641 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 build.sh create mode 100644 cmd/wasm/main.go create mode 100644 docs-shot.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/dnsx/doh.go create mode 100644 internal/engine/analyze.go create mode 100644 internal/engine/conditions.go create mode 100644 internal/engine/dns.go create mode 100644 internal/engine/engine.go create mode 100644 internal/engine/engine_test.go create mode 100644 internal/engine/parse.go create mode 100644 internal/engine/route.go create mode 100644 internal/engine/rules.go create mode 100644 internal/engine/ruleset.go create mode 100644 internal/engine/types.go create mode 100755 run.sh create mode 160000 sing-box create mode 100644 wasmbuild/_stubs/buf_buffer_unix.go create mode 100644 wasmbuild/_stubs/bufio_copy_direct_posix.go create mode 100644 wasmbuild/_stubs/bufio_vectorised_unix.go create mode 100755 wasmbuild/gen-overlay.sh create mode 100644 web/app.js create mode 100644 web/index.html create mode 100644 web/storage.js create mode 100644 web/style.css create mode 100644 web/worker.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d61915b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Build artifacts produced by build.sh (reproducible; regenerate with ./build.sh). +# Remove these lines if you want to commit the built site (e.g. for GitHub Pages). +web/singvis.wasm +web/singvis.wasm.gz +web/wasm_exec.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f7533b --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# sing-vis + +A web app that explains **how a [sing-box](https://github.com/SagerNet/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](docs-shot.png) + +## 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. + +```bash +./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: + +```bash +./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): + +```bash +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) +``` diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..ddf7344 --- /dev/null +++ b/build.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Builds the sing-vis WebAssembly engine and stages the static site under web/. +# After this runs, web/ is fully self-contained and can be served by any static +# file server (including GitHub Pages) — there is no backend. +set -euo pipefail + +cd "$(dirname "$0")" + +# The engine's Go dependencies (sing) contain three source files that reference +# golang.org/x/sys/unix, which has no equivalent for GOARCH=wasm. wasmbuild/ +# supplies wasm-safe stubs and gen-overlay.sh maps them in via `go build +# -overlay`. Only this wasm build uses the overlay; native builds/tests do not. +overlay="$(mktemp)" +trap 'rm -f "$overlay"' EXIT +bash wasmbuild/gen-overlay.sh > "$overlay" + +echo "building web/singvis.wasm (GOOS=js GOARCH=wasm)…" +GOOS=js GOARCH=wasm go build -overlay "$overlay" -trimpath -ldflags="-s -w" \ + -o web/singvis.wasm ./cmd/wasm + +# Ship the Go runtime's JS support shim next to the wasm. Newer Go keeps it under +# lib/wasm; older layouts use misc/wasm. +goroot="$(go env GOROOT)" +if [ -f "$goroot/lib/wasm/wasm_exec.js" ]; then + cp "$goroot/lib/wasm/wasm_exec.js" web/wasm_exec.js +elif [ -f "$goroot/misc/wasm/wasm_exec.js" ]; then + cp "$goroot/misc/wasm/wasm_exec.js" web/wasm_exec.js +else + echo "error: wasm_exec.js not found under $goroot" >&2 + exit 1 +fi + +# Pre-compress for static hosts that serve .gz when present (the wasm is large; +# gzip roughly quarters it). +if command -v gzip >/dev/null 2>&1; then + gzip -9 -f -k web/singvis.wasm +fi + +size="$(du -h web/singvis.wasm | cut -f1)" +gzsize="$( [ -f web/singvis.wasm.gz ] && du -h web/singvis.wasm.gz | cut -f1 || echo n/a )" +echo "done. web/singvis.wasm=$size (gz=$gzsize), web/wasm_exec.js copied." +echo "serve the web/ directory statically, e.g.: ./run.sh" diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go new file mode 100644 index 0000000..1c9d9bc --- /dev/null +++ b/cmd/wasm/main.go @@ -0,0 +1,103 @@ +// Command wasm is the browser entry point for sing-vis. Built with +// `GOOS=js GOARCH=wasm`, it exposes a single JS-callable function, +// `singvisAnalyze`, that runs the exact same internal/engine analysis the old +// HTTP server did — only now it runs entirely in the browser (in a Web Worker). +// +// Under js/wasm, net/http transparently uses the browser Fetch API, so the DoH +// resolver and remote rule-set fetching keep working (subject to CORS). +// +//go:build js && wasm + +package main + +import ( + "context" + "encoding/json" + "fmt" + "syscall/js" + + "sing-vis/internal/dnsx" + "sing-vis/internal/engine" +) + +// analyzeRequest is the single JSON argument passed from JS. It mirrors the old +// POST /api/analyze body. +type analyzeRequest struct { + Config string `json:"config"` + Inputs []string `json:"inputs"` + RuleSetFiles map[string]engine.RuleSetFile `json:"ruleSetFiles"` + DoHServer string `json:"dohServer"` + Network string `json:"network"` // optional: tcp/udp assumption + AssumeResolved *bool `json:"assumeResolved"` // default true +} + +const defaultDoHServer = "https://1.1.1.1/dns-query" + +func main() { + js.Global().Set("singvisAnalyze", js.FuncOf(analyze)) + // Signal readiness to the host (worker) so it need not poll. + if ready := js.Global().Get("singvisReady"); ready.Type() == js.TypeFunction { + ready.Invoke() + } + select {} // keep the Go runtime alive to service calls +} + +// analyze is the JS-facing entry point. It takes one JSON string argument and +// returns a Promise that resolves to the marshaled engine.Result (or +// rejects with an Error). The work runs in a goroutine so the JS event loop is +// never blocked while DoH / rule-set fetches are in flight. +func analyze(_ js.Value, args []js.Value) any { + var input string + if len(args) > 0 && args[0].Type() == js.TypeString { + input = args[0].String() + } + handler := js.FuncOf(func(_ js.Value, promiseArgs []js.Value) any { + resolve := promiseArgs[0] + reject := promiseArgs[1] + go func() { + out, err := runAnalyze(input) + if err != nil { + reject.Invoke(jsError(err)) + return + } + resolve.Invoke(out) + }() + return nil + }) + return js.Global().Get("Promise").New(handler) +} + +func runAnalyze(input string) (string, error) { + var req analyzeRequest + if err := json.Unmarshal([]byte(input), &req); err != nil { + return "", fmt.Errorf("invalid request: %w", err) + } + doh := req.DoHServer + if doh == "" { + doh = defaultDoHServer + } + assumeResolved := true + if req.AssumeResolved != nil { + assumeResolved = *req.AssumeResolved + } + result, err := engine.Analyze(context.Background(), engine.Request{ + Config: req.Config, + Inputs: req.Inputs, + RuleSetFiles: req.RuleSetFiles, + Network: req.Network, + AssumeResolved: assumeResolved, + Resolver: dnsx.NewDoHResolver(doh), + }) + if err != nil { + return "", err + } + out, err := json.Marshal(result) + if err != nil { + return "", err + } + return string(out), nil +} + +func jsError(err error) js.Value { + return js.Global().Get("Error").New(err.Error()) +} diff --git a/docs-shot.png b/docs-shot.png new file mode 100644 index 0000000000000000000000000000000000000000..a83bcab9e2630d0b87a007d56f7b144fa2847dd3 GIT binary patch literal 163355 zcmb?@bySqy+b$|niim)uAR;aB(vm}Wr?h}{cL|7;bc58;-Q6G|-3%e!&Cm=nFy{f^ z-?z?L>zsAIv(}lv9&y;uuKV8CzOL&gNI^~<6N3l?2?+^PQbI%t2?@Cz2?>P=?J@8Z zX)RPPB&3%}k|KgCt||KqPqlH)$WV_0U+lcV#e6cnxCYtkCIuxvTyd9K#FIf&>?x29-3 zSoHXIYicqK-l8(zFHDp7>)qB8MP zP@Zo4*W)}J@Xu;VEiA-fPvS5e^kOqyYs8@r%@Ff6G(>r5Oo6uF@dY^_J4Z!kI4(nE zEV8od7XyR;^gUl@>!p%gIm^V`0lu_I^?h|2DJk7kSoiRPd$Q)tXo*EcYa5fM{VIeI zI>x*8((CHgz^$3l-sS2>(V{2ngmRKr9r&Y>*jJ(NpHqMZJoVPIv^cahTKmVTJEgq* z2<(YsFW-z0M%=b zlGK|QB(;uLyFK#of2V_sc7i-B4j3>KooaIdnArk4ww8zNz1V{IW_`e`&Er}_zp5cA zDaq$W_>!2o=hkg`bSyR&a&Dn+>&{sX+E5k!k`TDneDd6kuvuAdWJUmWm2N7mP4BlPvvOLg;(*=&r5 z|6O(AL^{7jUbU;>t4uR9Giv!8rOup^iJ_t`Jyc$Px3gaVqOX5QDu+yiVE|qE%ik?|J{9F{kUjVRT>-gn;}#Achb&#Tt%kD(CwRZ&v1?**9j>5Jmb z%vF!w-ZR=ZO*Vvk_vbGvXT#pGa6-WfhU_I88A_!Q>P)}y{-U&6Ms{{~qsu|F z=jk;k(=B5P-^%8AJU0$bjBXpcL5{)a^bvM2AJ#Gv5Gyv{?`$j`Ok}Yz-Dv9>^iEKx zmvS7$OYJVVgZ4PBE9PT}CnqN{JQxc3!b$koXEIt2eIJLDEm|nVAjak4@PQN7e^XHh z9nVXC*WY)u14$5lOpA&Q)R`F0;??H_EI@qgm6GO{t}y5)g{Z?^BM(%{^OUsim~GzEfqkvjf>4Z5<6s<#*rbG@NSQ0Y~)UZo+o7|4a zCwWtck`p=2XTHTrO;Cl2hY?)Eu4QBUx54g?)ip$rGW^TNPYqK|zSAXhE)BKOneWsq zJ}9WEy*83-+}JrBt8w5W{e6F4At{tlQc(dh#ZdoPVuSc*FE*=3?gIMfU}R~lVz=OA z+&`V;V-_bO5ZNgEQn4J&f8&YoK{QDBxP+idjbqa|w_i$Xch@}r^$=C)n;N4vO>}g0 zHO&bs`A;>L6*Y1GweiLyP@QnnS$cZr8`|4QYf6?C!~RRk@E| z8#)olE-5w3LKr7RPRD}*NMgx#(62B`qGSz1h?V2!zMJ64vVK;9$uJy*%-8L-H!kLp zI$NVrL#Y-o{F1^jgy;PvO2WQjn1#2j@8g|BIJdrnYu~QlY+-TmkMN)htAmtyFe5!Z z@g*F)-{qkd|6KNlIFnnNd+vJ`HS6h40NMQUq+0u`n_Yx0Z-oTBdv#3hNFd zl9HC5_{E>fEtATYv7L}EAc5B#=3QpKwbFmC@RrwQW&GEj=jIT&-RpH6Hd+JQr#7nO zg+s*n+dSOFBUF@*nmrCP)BL6;u7SS3s~eD3#kGy~VTb%h?L$ZMlk!O~VFBL5scq|% z!&bfNclQB!waK7Of=@QxJ(eMVNa_~Q<^c1m)4ly-^aW0~5TCH6C*$`@VQ_0CiHHay zCo8K{;l7YFpbiS~pDwVPDUr^5bnBIIW@>Ck&l~+h_kczWrTOx(@aNfC56lLQjD-l7 zWOaSD&z_0Z4Zfb3zWmvOUb-JGpyqi#>xU0tyZ5hf4GK~uI^}PyUO170n(N$HPqpd} zt>=ypPw|<|2#hFm82K03P}DT&;3PtAoN%_8^4B*936IC$MnxI0-kh+m95&G%eeF1T zg-zJtx)aE)sruTLkT#3Uro8yv+zfpN7C7({co^%C6XffeJ zlXI)b;iDWuAoF_H+2juQK)v30y5F@}PyER6?T#R9FgxYI@FTS@{Vpt4rqd?rGYZM3 z<o^c*K%h6qSDY^s9l-(F zAa=+kkJCY1JQ)1*=cj>ms&ffRNvl6+b;X0zOD}!|hv(|&3&sgIj`R<|3YB`tIrGKD z1fS3URl~$vstdh{+S=C()YN%jpFb-rD1)&a)f^q^cs9>@G`3-UmDO;8eg_pq{~2DG z>>C5C24g(;%i^i1eqOtelspxaIDK<>g!SZ4VdvmtET+)Y7Y`W%L*G(`^Bv3XYEBW5 z7K^p=F7ke$(X|s2Y)_Z!2xtVcd|G-WOnb;~VJx;$9~c2vFp237V`FE7sHh%j{+Vxf zGC61rD>=ASk3x65f&Qub7JhIw9l)GlKt@bl=P<3?Z&u5o^+`)(duLav?eHoDI0*t! zP*R)c@EyKu)t0jqY6W&roF+U1AI#g#H&en*#nZsRp(r6OZE{>09UYx7emxg4srnrQ zo`{TY$5Fszp&P-lIM7xtGgiMQT|!jJ%>Q@Cq@%Tk6FXgBw#P~1^Leeh+|2rWHr1UU zEL8m>CuYls&7JjeFqEY69(!LN*`>|~2%}=n)Og|y@M$~j<+k-i5GF?ctoK@r2M#we z@t*pRbt~YH)`JpmZdXk(vJYAnn`4Gvq5E@9sCal3@7@(`T^6tw&(MTJXYb^RsZrgb zK{0van276B0_`JKj>0O>Eo-8!e)c_FT-;o_Bmw0CI%$8zzK9wJqLD>@5m%LZ-7BGE z{-^9tUqsZBs*_jX-+Pr;Y*$ydx=&fjhvX`{!_zG$74`L5AfQQEyV5xCuzp2w6zYmFk1&|MdNob3TwsdnRy%LLe%8gv95&i#0JW9?UEBBk;FzxM&)J ziIF@?)oz}51p%)pxe_ZNKDMXfp4W}sq^04-&p9mrSgA~p{_YDGbZfG^xidt3v!g&M zmtwXWPU}QDD^+ecKL0pv^o>PlBO)weOyCS%dRMsice`1J*FK(36e0u)j?NRpOZx~reEH0iH z)}D!J`bhuedZKqI$$VZp+c{1?In&nW{;!9~@Y0RLN7JyTN*Rw~m>U`KQ~8ID)i}mO zMVILP>rJZS-HhJ6%wk+@!dsSqOnVkPCHwj=N;GY;Bqe^N@q6cZm@uuZfWAK0YrE33 zVVcH%;w>p9>2|hmCMITlHwK+`$xcn4$gU8MiB@7^WhIP_Y;M-}h^T$eQ0581+`nAF zeK(f3RC5Jr7`qWWSFbWIIV*UyyuKlD{3qPws4#=^4?oVOq@)<;jew4^($gs!7}(7v zmc%eap9Y5NwmRQbczw)~TAp*D#gAlI5|g7bIMGgtraO47Xk_O{Ny+ysq%G<*|up#&WSy zQC>tx=6l0WPQ8*N+hTj{>HXXrXB}f=V)z{QWh}HT?vA?*ic7K<8l14Vk)S2?pft-UeLyzh1&NT|$_GS}F#)y%PJ>n_OUC5$UXZ%8R z^s5TH(r#?3_8c*N1mmeYCW5 z2)>y-lf1_h>E+#+dz6%UU0?4yZVQUCjI75myAne>Itd_c@SL z{H)@ifeV?+z+*tgP>e+p1_j8cvDwf3TI4+BZMMA<(;vzFCVR?qI~|p*#+1n>A!xC& zq@|_Fk!7CO6tXrO#sj4JQxHAd{zMnEa7av4Fl&eG+=YS{n9pf{*^6&Pr%})+MQN$G z-IFH6Gx!JhhDV~BAgVmR_3|%Ha$(f7Brc;+#W%v5AKb*^lHxZ_O-`rbI|qCz5h-Um zM3#4w?y!}$vaGScfl;I=gsI|F3a0HjEe`ahmtjmau<_id8C;0?q?neq>%VH zL&a<_gheUG+nPB|;%Q3g;9%E0uAmVagX}r)6G(xTz)lEJ7%{;Fl^1YbnvBi2+@5Gp z9jCy1exRxOn#0~g`qbE1mIhmKAykQQvL6aM$;+#@D_6}YUt)lvjo_Cpj(V0^zoAWH z)Tk-Y_gq^gDetH39?@Gx=6l1Zb2#sPMtRW&b{DJZ2_yNLlGJE@T8cjLdVk*IJ;f4s z#un33x;Z$(V=ku%vC@6fdq`Mw+V+kCRjTAeU8XA{N^hj}O+l^X>FUnZCe)X@*wuQ; zLuEiI<$fts|e?#XH33QmX^X5d7^n9--i`hl}3uoF>k?N|FQ3QRRF+ajhz4 zR9GA0j~jHAoo-R;hqq^7UAcKTgH|D>QU((}eLO@yJS}JIT!G z53;-qp?d@Y%~&~L>1k_4uCZb>3OZ~Lj|MTZ-)O5Y$6L7fQ@o5y2ar&pFCle5nKXMw zzr9*mSu;T_)@KQNQqbYqqdH^I(9+prLT5n1;VRIqPxDbrhQ0Qd zd!R$S(pjDg(Va;Kv+8mbvAtS_<1^#}8@q8`_q)-E&l>fJcD8}eSf{d&gpknpU|y8# z#?Ur9M9nPg*UyK%!d_Qu?b33RlG4(7`2uz6kuKjKZ8y--!K>Et37qzz6a&V&HxVgB zeAQ$(;6QosI2+ny>O5hdt))$-4SX&HB2bFPO-U*nL)CO9k9SQ{PIALbAv-JYnSUB( zLty!E5=(%m%Y|X9ZC~QML@DdSsOad`W7qFj3`Y%g{ZeYLjfJnzKmQhM4_$>#kLBj} z_evy~`W5!~vkmG!%iZ!eorC&T%YfUy%2IaC2Cz5Cwa;eHel~~F;du3LzPXvLbAV1{ z;fNkvAxOXc7A_cRFZEx>5V+rBNRx7jF9>s7GqZou#RNa5-;CfC6A=>^`>i0Xt`!pe zS?Tx3@z@hphbpT1AO{sjkE7nUa`IbnOL{-?03XXleJMF*% z^O`U*G1+AwwzJG%F3*`nT8Q(Sqho)6|42S6p>&bk0a}@ttPB>PR8ifx{J^B$&}YvI zMpPvL;VzRom=N!^b+=~1^q81ndgv5)m&?8*VZi2qZ&pcIS58e%Egp~ShT+4|h)%1V zWc2vjc`{5>lReAJQztdjh0GMAK=0C0tY1ZuIjzB2dce3$7xl)Ir!YpS{z$RxY38|* z{0bHc(PrA=<@jOvXnA~VVId2iT*JI{(V7)hKczYnBr2vTSRW{V6E=5&_98c@=Bf+y z7C1F~z@1`F=63j1*uDM3*7ND4c(CXgWiU#!!S9Lg_KSUgi?nx{pP-NGyq>5F zz_HrmYG1Vd&$sCD;I=vu(ch3Vb(c@pw+hO_o`XksTF%gj;5$pxbWXE<1v9gP1=R3@ zLYf*UOVKCK-;7%9?8=4_5p02y?PgPczgk`GZ)g+#SZ?D|XuaVr$^@&egYvJ>s`O`B zIvjMjj8xdUb8C_^hRa=WxCP{iIKl`0f75e}{`wGjez0@8-B-m|YfYnSz-hCT zm!F@WQF<*MAy+Kn>2XEE?Ywzia`P(s)x2bAM}Dj385(VCI2m(fuJgD}o3h&E*p#i6 zAz9YZ#&X5Pn%MJf_pC4{2dPcAfV-9gxj~YBO;R3Du$ECL>TUGaMx)b-W~DjBQz2En z4fmxNe7B{!VZ@HG<7<Fk;Aeqz@@~9O{WR>UY3Kv`BL2$>?=}*P z*@|RT{BK?o@t3_dZd-G-*7rjq||3+MILL`eFBz9@?S zq=BJ3n1&!z2<_>Q(4fglHN(l6(s)f3nGZK(!7~Xlr-%pc?nm>gmDGEA2g>SJC zM_G0}HNL)n32|@9&ma{aGDs$a| zc&rvHEGziYad8^A)7s|RO>Q9SdV70$y*9ruP2dCfhgnUc^H6gwt)Ilx>|YdKk}?%Fd+Dz(G{H*d>Xoy^InyY; z8MC`{hvr_xkJWjwrU81`0PkBa9Q97u0Q|G(wr?TNIy_*YR$Nl4gY(b7Q_)YWeZULUVc zCND?AiV_Ala?kH55JNl{idg~8yuW{!%SYj!wsv(pnIBT|ZqC-2+h@H3U%eWP&i2a? zlr1gE%Nk<8Amz5(g9;~wCb@HukB?)e@n-P`34C%U69UPlGJ7oU6Q1u8;`6PyclL;d z5opc<#I74&JPIu`RbFxb-V}F2hX>bTf1N8I^Hod>i6%9rnPmodLk4f%aYg%LFMvwE@b?76=ybu!3tc8 zfKF-dc!QOeExx01>wYzwt=H(B>^`j1XgxPBq@^tU&*qA+dr?9B9Wn<%J{RBwZ0$V9 za1)0a2Ey{p3>(zPWaK1`6pR6)meXcIQcCI?8d*K4UZT-vu#?GqJMu*LgKlL9;?F73 z3A=VmYOeg1s+?SS(YqJhumLz%o7JCM2k3WET&g@-aQWt8m^XtKm%(;A;RF>B<{vRJ z$cU1_;HaedjEvF`^vXIB9SuXHZ`hU0pbTcSPX?1nzHq-Hm{W#0`_*KMP z24Eic^nDJ6JL|wZ7zX^TfAfECOT%LRhj*B1ZT@);$oDQlcf0$2e-oK@-+km<52rvx>4PUAKXojAK?nVemq0@R(;C`U*zbQ{TYXkN3y+p>2)7qpCOOh9IdRUVg(E4 zy^nhHZkWD*TmS^uZZ@}MkjOSk212do|i|=Emp-Pg=%j<>?dXNk}`2o zV*ZWEehFzEN;Y22ih$zm>@QckF4cftfR2tvVLj`+R#7~n&PVQd9nr}Nw&EC&5)Kyw zm9)P!tVAAT1rA9`%El$!Xv&2~Z0V(l-6F7F-a0HoDoI$95C*Iw!a7!~Kgspe-WeLS zYS-KQ8m!!P=DkFdF46D2M!nci7u)VB4^lRUS9}hyz(%O=6SEiV%_u*&AUxaNJUs;8 z&d+WwR<5L78m+fY+hitx?oChsY{ei;_XOPLdIbn4QHo>Ucl_?NHV*l}B+fel4Fk`? zVpuii$jx^?D)WJRO&?@~#ICL4SoDVAyYlN7#GR9LJSJNmCU3UC9R?tlTuO`S+_+Lk zz}J!_ca`HTp{)!F3tjPdqYVoU%qy0d3KA*&&I9AV^5O-ap4 z!$2u2njAj`QNa8v43j0|%uZ1|N*7A_L&9gUy?7h=f`prHdu(Dd<3{dHi@Wn-KPKtv zl|xnld|=$(#}-hx0l^DU*FNYq#LvRL=%1;@;d0Mhri%kd zk<(Kshogq8Eg!z*uC?CrI3~hFAJJ*voEQ@p%n$=e8<%~qucyPl)OU|gPAbQk^-Rla z!%JuiE244u)d4IU_RbXhHt#EwzjrX(=yY6s{EJ%xIllAI-GB16udm%b-C;AW4t42y z>Jg<)Fw*qpysQ!iJz;)w@+eYF^mT-NP#h&yc5P87<0)TF~Dq$IM9PwGpCPOjf{U^HLw$z}!?QLvZJ*T=-)2(OfAYRZwYW`}18 zz8DCSmjLd6ZMyt9^_I^AdDJ7DJbQ;+MbCgcxh4l|*cF^|eF4?Th?3ePnf%t{pL|C{ zG0ZF-qVJ%+X;yx>FCaW(tT$Y8ibq$c__|=|ys&XD)yc^Tq`bF;&mQ*b)vFRq%}Tng z@{_kNz4_HDPocICaji(fz#vMTI6p--t-QQx8uTL$YIfvs`V5W3#-OXPL`lZm)U`T5 zGiU@hfIwg8PlFHiW#pQgDIzd z_823*B;#UV^+&GzjyL8BDnj^5vjYo&7Lb=8L5qM3)wD}V`{pCx zPYc{ug;+*Hrqmg=X0jwlI#EF;rGPHMo9ctv&`Y0}!9s_MMxy@tKWDa?&aVVc<~sPSQb|y-_NnJ%;^S+t4>7lxwC@OBtBuUe%=ULobk?}BI;b6+u1dT3hr_kN zMqIYIJ}=Yhq-10?8;ln^+@6xC6XKJ#nGanPbJ_%YJ)f8yb9OZ4)1RjSP)vmW3P;J@ zI8xxnfw{g_g9WUWuxgWVvc%zhm2h`|w4nzFgb;jkJ)FwUH-fg%!j0PO`lmq|72Ph# z14j0qlDes(`quN#l9raXJ!_%SaU25Sb=wWCu0D%R_Z97e=J zz-s>!a2z4>Mp~Mi?6Y*59iJ{BmkF)SH``eqn><`ZVr6^hT#`A9OB98{H>~{R`hAtE zZUGwgrs!6a5>ir0No;@vi^py^v=wZ+x1RH&{e+9sz*!YYfNir+7r#=xyix&2nU2MTH$;tdURS z%L8eB(!jmQ%#*(X^LoagEG95j00T{G`W$tXMJ<=AQ@26}U`;!_yM*pRu4h{e@QD#b zL2bO6lG1ow(KL_Cp6B76@Kb*x=Mu~MJP07Q&pIkAD$?$YhWvbUp9K_-NqN7@z!27m zt;>}b4;QNzkF{tQR3>N!;O(0(ora`aXQ{nlH1tkAxf7;wk^^(ZxN}%7c4M*{U4Lj# z13={24yX&#(Vbg!y6@JOmz!I;iv%n;#!BHWAbAO+!bbZ0uj!ZMiVdmdT3oM!&0W+P z!CcO!hjFKQ1*a=n)c8z#IbzZl^o`!|t~4kxJ=ul*@&+!@Men0!?69y|K>U2>VMcU$ zS3<9Hd*j55*kw}r{2UAM-Zi#o?G0D?&0rAl7Qh$-y#;%s<~!_fuS_$hdfMJ-cA}hb z%hQ=D2}ZX{Hsk@YLD;HMahx6)JNV6gUW;;T6^0SF>BOvIdY7QV15tqvG+@!)4@NdDmrI7kmK{@bikDZa$lWpFVa~P!>m2H zbeSD&DCQObA~QGui!eJQ!)pr0n*b#M(Vuj#0=}qE(7FiOz1nJ?{>C^Q7|lVH0bIaT z66SKqhxY6lg5dR3lL;r_F}gO$BtB1Ovr-bN7@qBqeik`wMWO+1-yYFx#^s%Dj0?*V zWsSedA-tsevFGTtNtPj{DwzBlo^o}zSMIjxewchUKqgHsbVyr86)DBsxu5&z4jz&Q^4cyGv%vy$hY#<4r{COil@o723f}!&dju z+#@DEDk>_wCp}$X)fCmB`wRMjmmyE?rWJitPa>j-Fu*%F{pK$|To)8W6>xjQXYdQK0h`*fsY?w(?`W!q3*gU!e5v8N@^1!&Fgo_;pfi>**x0OB{b2_^+Z zdUB$Ih7I7HvA3^0GaY!XCfYoIo?Iuq*jfa*51=im^`z~Wf9kH+r_J(B2HCgi#|Nuv zCt-lUL*6?nF{%6&o%`Q$B>{GVYU)OC1@;0P8-(^{cUPAJqJXtkI=-w>Iq%}|)OZ4X zym*V^K)w7jO%5*L3STccIzq$4!*k&@*@gTUW<qRNXX!N z9bH&F90$5Tv*-YzNce!)#lDI8Z0v>3)fGrqPL9iI|80(7ycSBf00A+viP0ZzHMObC zhO5mIp85q%QFSy{7M6Klo-sKVs1%1aqg8>o%;ik`HfUbhslxi8+qq-k`t5BVSuyZ zV6MfgtGpe+q=0{P3AhMVx>aeg<|-IP5U2Z3PD4zhJQ}{x50oX~5C{8eke4fyCu`-aYv}&{)>EOG z&05H*f;GVo4;MYs%kp?Ed%qb;-=*JO=#t94C@~Yi+O~^HVzbWA&wmU5&>kmI@V=LQ z_W^pg$x815E1-<~^$zZzkjGQ>qRZCJ1?d$LX-P(|HhkqN8A&PmS9zXDS6*Fn?h%19 zcSwD`9?|~TkV#+KvwMP6fPDr3rR8E*{*Omg^2uB-Q=zAss3=&B2_uD zXh1kBx7j*JTdG2U{%kb?XZd}{kyJpjK`g9pN=l-XNj}2j_HCJi_ORJ$gfp#Q;WsL< zvCpmPf28SB{F0!uM{Z zFz@V4hO2>$49!mQuNyz#tC}6sK>?~V524Z(SC0Fi8vYRyGC4lsezSs86mUh7aP}WA zKxi?LcnZQeHmOI4_ODrAMY@nKxL=B0Xi>dNMgpfevpB!7_*WrPitq#fhBO(M6#Q!P z4_pr!fY|__Hqh6P>$xySM#&lj(2ig*7@PFU-`rRAJtoV)%w+zQN*rDs9$uz_K~~%Q z?A)AFSB>J`sMy%Zgt(}L7-kFYIyw?sI)&87`lEBB;&)7|t2ixuDPNhEPHbx@#ub_z zZ^4DZN)8iZ4|hZ)k?8vRpW*RDzR~ouZ|7YgoLpQr>Iu&F4Qe@hiKB`zf;x^E3&vFd7U$eE~bC^_!x4=3H7%zt-v_x1JG z*Hn0(oT&_b7 z*jQZ6I!ig`57dDR-tc`L(~qi+?0xVr+vqqwA zYtkX!&Hg4>@N2F!XoJ*u7ysq<{-~0uWK^LQe%VsaAV+q@$kaj)w?|D*f^ow!)rcu8 ztD6bqeEB~%2HHK#hOX-k19gpVO^PqFe+i>2E9)?DQL@wTA>{tCQfV7DMBo!9@|w~1 z0q&c+!_N8HYJ-NJTrn|kXQ#@jxVQ$VLv&e_XU}lx0=;0D-7}ZSbcXh*HJkXau_ymb zXk?%uB&Km%)#Gq2E~{NK3Nq13in9eI+4(54B%c7xNcgs(Sr=0yN!47G zZs3sGRwNieh!i5*JBdE4>ORAjQqTt7_OWkksQ9prPN=Cw=2duJ5(w%?c#agM*AeX& zQiX-?Ozr{q!JLAN3*n#j^#pkJ0RLM+ASM}VpKzp{|54JYg0lffa*_+EErHI+6@L5k zDM_hPRZXSb+W3TO{Rus|EBi~K@Oa5D@x4j*_3`C|dcW9wp;wSqX$+?(e=F{U@e=N=PUnNBTjQsh5R{&djq}fau-OjFY z$Fxvf)SGd5=HwDui0+1nDom{=FdIWxqoMD7_aOUx3sz3TAjg~726y01u0la>y2|Py zd-9U9IoaZFzBkGm8svXhbTE&U(;1{(+<+O*Ws^)(ZA3K9)nY1R@?CqJ>R@g*sMM?zgmVIAT3~+mM(h62& z*v{-ZF#nc_h;#?GuR56Uu`ppg z?Wrz%;N!U_tQD+VU9|J}isiU)jjnMVk|pVke}Js|(Qsg`sVD&D<^`v=CfVLO{~d5Dv+o7cu+B$f=c z(K?NGdPf}{+d;AR4Gu4!sbuEB7AiK}&JV-2)xTS z@m;i2t)w2&oh&R+o)P?sfPFCRJo>Q>1aUox_K}ifP?&VOJ;`lBZsrGS3JTgN7cbzb zF(=+mZFAQW))CDJ;GO^$oP~MB3&x0##PjZ5V&-be9H(Kz39@~faDmf zyGJx)O7FOB;k-{HlCEd14z$V*(m(%cAM)w{reXrv*)z(`Z7c-)*2m91v=u{6%!xhr z;PS;LUtEDn*V;pP31#K@?@%DVby72peo+{U$jyjvm1B4UoIrY(i%xz`?>F;$e~y;_ zfY-2R&9diYzX$T#-7luM^z_m?vN;)csl1&EPbVjG&ci(9ok>3?#-x@M7n@FH^PkUs zN+N}|WM(V)ZiTMa8Bx z!j2MK7v=_fC0J=dfRJ>h+q%#s&7=0lFF@QPpBCTSwH-Ck0&Qs`9Emg{A!(R(bmGfH zCfx|E)!g{nMG>;FJiq*$!~5|jTo<(w)t=zipfoiEawdmq|CN>7QEf7@(( zKD?I4M1TDkDFN*1f13ODgYFn>Vr#t|I7nNbVMc_=$HvCmI#k}05R;wohgxHSks*Zz zdtkz=jB>bd>7s#X1ZC+c7mw7aR zfM!gEnasv!&;TOw-srxKScwV_X5XDG`2BCs;ISWHii!Mge&KaE)vfmLnM+@k3wBmH zGjU>%+sqBC*LT&>01UuDvUJ5J_l`j>~yjPKji%ay97Qb{+ zQK@)kV$r|bZDaF^5#>{&gD{?mZNML=vxXBxLpT+gVEFgN3UX3kd-H)~Yon#Q+L4)A zY%Dxl>rOwvD=#O5ikjX~f9jKv(CTTz$I>=k9brby&0|VN@@t1;0B{)z=~#tPVeX=! zv=}zvM`wJNOhs{@1>Ax6oFnj|0a2E|G`A?^{HN3tHpaR%P2A+ggXW%J&F zu}IHGw++JYg;D5|eMIJT5IGqs>gTr=U~f`ej|l_;`6%30z1vWP7oh{Q@1smTYiYFWA%{k1u%B1M zNV$p>gylpv3d)7~PI-U-Z?XfXp^c#(uzftmzXIU?%&9t`s*jJ8X5BZp(VW5afsO-4 zl|vcdsk(tAqIEAdB-tMa;(xWR4>A za0VQh=4oEQ|KWR>ng3G}^fEQz{APEbe;@Mt0>z1V2;pg$dQAm!1!PZBB9^buYo<%* zy6#&LSdZIuL3@<`{bPeG%F{qwD|U9(7^<+Fi=B<#`GC05wkFsckejBSp3VF=c6|Jh zUYl1;Txv;r`mcxg`@IFYAua%uO+ltT@Gh@s!?YZE>_6AV8J)fgBmV%$S@5WFNIu*9 zWd3n>?qSP)`+Cnu$E{5ibS~;>ku9`XTdE}7@j7)UKvA)XtrY6UfC`$M7X-6igV)O| zhCneAG2+kW>~K>?kbjj&AYRekaBpum`OO)b{dO<2`DoS|ZhIKd7d9-66dm`o4D4Ea z+eE%Y7?2d;owiJTM%Vj3!eSikHe;>Ib%3m8i&dgiS5T3N$0r3_)NzOYarw5FH=Q}m zeJ*?Vr?RCcCRy~^&7=kAYJt$~C5-_0>aNDQEh4~iv4~=3Q0yg4@ z@#KF1F50RpA|hW@RM^;K<6@Bk8j19WyoBY}3LPN1nB0*)!@oVhI!&{eGS4Fu3VRx~<)2sr7I+N4dqld(IioqknSNk!x~z z=L@Ln&dwZOcOz7CiBYMkvZ-X99YmbwA03a7T+|bOO#>BDKi53&GM8z2No|g!@c?B} zacm4=o33+~BD1sC&(6*ON^jI+4CxT$SG%|DaMwYpH5Y#{ufwF*FjZr+1XNZ$C`NIA zx$Ob9;tb+xHATgpu_5sz1%PJ7I$WXzOrhwuXZ4bhDz~*H<4t2|ZjsG7m;8#e((4Z_(z9QLKtXT!F)VRafY_of|AWy)#o1nWuyPT*|%MH{; zHMtyGOmhr^Y(3;7oGjwSd%9Bi9iYP7JxB7`?C0mXvRA628i~XL8X9H*yRKfg0Mu8# z=C#<{bh>aZDc0ihyv7}y9ggo75dGa2U-I>A8-0LpnwgooKv}#@b>Q_x8m=|1JSmS| zp$V+?;lfWx`j}iGt}|6w;;K+GOH7B`1)xE(c_H0n;PJQY$bo7^kYY&~P!?DZ0(m`W z%`ea!#J~Q@6TQQ_H7+}Pb)x#&$ERG-*WIz~@}QqhWiRUnsLVS6**=uk%R#{KxXnXtxuWngR@8l6=@gG3;UkCR_GvfBuY!OQHM)_H=jV<)V7fp< zu@`IZMfkYw}nEijC&AQ%|RlF|x8sL~#Tr+W_TEr@^^%@=s7)q&|Z$EA@*y*jc(j zli6u4K*O_JL&~S>I`!@`d3t8h#`^j{NKyFzD?OJM7x#PZSZTry#Xad^SER#7)rwM{ zBZ^7T(eHM6=YzO9U>#wr0{(x?pZ?$fti*rQZYPO?q$=<1?EJ-1+umGLQxgS1?)M!R zaH66m2OIzlOiV>38FY+{-G$HZ_sP~@4K^UDGFS^V?^7_fE96{-{7XEc0yE(M5wyBfMkX? ziDI}G4d8%>jS0^_b7D7K!`bOBL;_x2_zsYh0i%c>{)2I3nOgkmH!QNO$^wPUEZ}c$ zsNh90R}h&?JYtV@_J3Q8oLE4w^hPX-dVfe^^)PmJ zzL@8#T5R!#^Y{I2NFh#)Jh51-HYSnEbJ^OPE;GC2sxrao z=K8#xK3lW5p14dcPrB6L8G`YXj*c!|&Wf%o-~qjf0^D%E7Y$BQQX-!l{O4nRH8nMX zU#G3NC}@xMt2T#VhjYn^iL{iIqTc*oXAn9(oAte*E&@1ct=A>A@&RWK$`&k^tyoGXB6er9tx zF%Cfq26;PQjZ>;~a#B#Njbf3>rSVfz(oU8uQv6*zB)|}xAOC@&M6daO(Dv3*RjzOT zFt(B^-3OGGl9EP|?rye-bV)ZT2uMqJccXNdba!`m!v;3&_uhEU%y(wZteM}M_4=2` z1A9NPpXa)->r>a`WK&@TX>)mb>ERO)xZT;+CI5W+#%1aJ_e{Rp*B8vMU;i>6l$Vz7 z8yd>hzP`k|TXhp3PZVsISY2KH`t|F#7IVK_TkXyc<&>3kj6N)Et=*jDxEmYO(j{tC zm`_bk$|=ceYuCSR@m3xi3ntS@kWsTo2!`xhk!`fKp0^K7??+A6R4 zFsj~IHuo#`mlT|nJu|h9a+@14`>v$ILJ~dxCOmKqSK9TgXR$IEuPo-ZmCILB1 zey878ZyxJ4*4g{Ktc$mgPwqqTylr*O^uYEKrUwTH6S%Hc9KX}LjTb@ugM-slMKkzw z@B9k=5gwi{NexxV?gSNCRcye)@TjLZ?oV^#glM*G^^EmNu(83-o1UEfW1WmbZ*o0oyw=N?D>rpDSZ5^Q9o9}wB(O4y?~^cuHKbvD88N)$z;@0CD$jmEaRguw`1 z_>8uA1&&=>uZ+xN)Dv;LQ>=zB3lD`K=EsA(^_AA;_`$O@1YI%(2Y+= zM<!&HEGZdTPDxHtsqpYn;kImlNy0MQ$vLjL#8SonG7tDX^%13r*49?YBOl{z8VLyr z!0AYc-$1)%ZcjC}0|zyuO?-U(m_f)=4!f$*&|iq6EDQ($dx5@%d`Y)k=$^a(56vn5d?vq=-Zy;w6z3U;e`-{r{g8^}jsS{}uL-9W|S( z?FA|eJ|JthT7sW&1{O=ZVk(#I&bns{u9K8)vt{I5FZXBI0K)U}nI|d6KAU(Gt71Q0Mn8tN?15f4uG+Dm*SMYHN^ zE}#Sg`~@-ys*GOj$*#*V*QnW@puIXG*XFF!{4uT5J6=WB_JNu<-DJt5+ZS zCb?4v8%&jUuJ|rXNyXN}pKcA-TZ3Gh6xW(p^s#~X{F@WL!$2omsCj0PXzA^Ss!{u? zHLcwZjDW`D>@(2wE)dny*4oafRgaj#!C`T_JxVxGVNbPx_Ue3L^sYhsJUm9`98&p= zoxS9FK&=^J`)=I0!g6y49cy!O@n{PG6WX=^AdZ)^8x;?kHW)t!*Rq>>Wr zY#ig++MI)(ftL?zkyAyKA|f`(?p?p5tJvrJHt%}E%1Kd{naEBr05U+-%~@ZReghEx ziu0zjK~Z_^RxVaKpfim!!Sh)-=On7DoKH4C)#jmbV`Q}HaUrjmO-4o56j+;?h(2od z4CLx1tG6Kf{s(%+RVX;EZS8>s%%1=z@sRCwzTO!Egl!}i$72a*@kGCWPZZ8x1(9%z zC2+%MYg|^RCMw_5DAQZaRTDlvNZp9)ChkNBaWL^j<@WF5BbY zSEtLw22q+ItG>}~j=Gu?mouKm;>f1j$p*Pj^X7D;lXrk?Fld^IacsLFJJGE{*9uDF z0k}J>!v}r}Aus!VzVwVN_>ct?g$$>f+CIAW*P{7=Yg^=Nwck4%+neY+^FGctKpa-e zy0feP#2drmFw|qlKdR`}t6055hesI1y)5{^3@6lcI;5AUK3Se`pcr*H-26y<>V-F& zxxTs6>QvwzL1AFWlnd6PbIU2v@pL*Fdf|=U#S$8p)qh6(W@mGFJ(0v5B!xUeC(eMz zMn}KI`x2Q|8c>>$5SRon0cv)j9WD7dEUS=m-}ZbEVeR$ki)OS8|4_x+k>c=zl0mrB z?<(x5|#y(w(nE0CRm@2yzM!|}}Twp-1%>cqU<&ZsvTC6V3)w%QHXOl~V@5!Q-=+q%dZ~pc+Voul2 z%AmL7=!LA^cfK~A7`nV0nv~#>A{^fy%^;>|w2rc{?iUx81nTwOoyoDWu7D--(^6}5 zDce46zFqIr?RQ^y=hD^z|8wnZ10pC`u$PW{0QgFo@f%X@gg;(jd(r0VSN5Ra_T5qj z;rXi`g7b|P>CQ0jpFhYMR(XwOzbgSU*4cPCbYygFg)!!M+z5LhG-dVk%@(nr#|gF2 zd*p0ZGho?yPR2w$(z%~{_pUiVcZkyB{rad;Ygn(s zZC~j6d0(3L(9+Dls1J*6f2K-gR03|$pEIFaVV-cPE4)V9!mk0f<~SVx8IEm4vO8Vr zQ|l^16N~eVL*QQlv!HiY(5&yVmrnT(*%PZvAC8ZtI)1XKWXB7YahFh@=9osn-?MF{ zxHTo=WCq%iatWSR-xZN)<;V@P+U?lDb1LC1|CN3!>TYauXcDJry*<|%m*>5t5VgPa2iyPcXwPBD#r7Ql4!jcM%I=UW70=?}57X^7Z6c%?$bzbm@y!j8%2 z2?;hV6O?;nYagRZi%F%G17-8DciStD$+k8M+2?7$BMf4VI*9zvmzGY397$3icd;2R z4!mjj-D<56*S`JXCT~1X!+#3B9H-Rf*RPVCjdR#4VH;zc8@rJvT;UTd{mS=^ezP47 za8J!l3=M^oWW+dO!WIFf--|MPesuUFDh#M0q&u#HJq{(r#Psx!0HPsa>kV)&B}8(e z+_x>=S*KIos_{^@suLNFIGhWg^j+vfG-#Acj+`gtSgcR#0l-jgASIc6-2l6*t_C5C z^hdSGF4wim9y;{*ZtQrXl(KDbjxSJzaNGJ{UP2yaxAL1G=uV6ePw4kWzx(=wfWa&w zmV>3A|J~l^X+Yrp;hgkrJe)pvaJH~ZVY8uHw_iwSL)8{`u*hgBg_*KEe6fVd0!=;} zkgYB5ZM`zwpLAXTa_mwUKKGmE)A#Mai}x>}%mI67R#aao=NgmOK#Wf1m zCMpVwlF(!iyxHsyVaK(pT@kDgSjHV$YmvF%tEr0*Hnfi#HRFBf*cR19WVLa ziH|8Fj|i`0A`_dCLt|qpv;Bu03?swB6o5X*$Zb>jSoBW+{$w6Vn#k4`$0R+Y0h$)u ztuA3jE1!Ii&1T5y4KVSp;Ha{z1Y<}jZ#H#3Mc_%<_qoy3QE@)BrKWq^`55)rKT zLU7anCSPm1cw>^rqdb#x79}Vvqm{xz6x2!kvoIRuAwx*5OT>CrXjLV9Z)<8pq={j+ z3=x0ZMD||%g?p_FHq@5s$qmz_7twdoCqJxoKkvyewU^I485qARdPq%{s4+#q2E%%% zsadO2$K}?4ui zyFR>JRVXB>weG8Ho}klF@C_A@y$ZVg^-eZc>JrD3CRV;MZ8K%`U3Z|>U)b0P{Xs{! z+`(FIGAp|hc7{2z*0FR{G#;;(c4AUs4lUh_>#|(QQ`OOT(o^?AacB694kk~_{hO8> zPZ5Co!#zcUb*)g8L%m2}$3oU;x=TTf-E#P5N&7Wd#v2)Yu8%twCF?RtoKaICN{q(p zWM?h|*a?xHT_2$m^EtsfhYdiK>gD;a)IHO=Y7p)!0i@*irjw%6?LgJNY#iF`wmB;( zB-9GuJbso&^-8-s70g?#xyl8ml0X^yX(C18+^C^91yxuOosj2CgmOE2q78tp6M$($ z!Os~)Z!V+&1KsfbVG}7}5?zabfI)(D%3^6`1k=TBjgC$%>;svZ9;~9JrO=q;zS%1} z6swq%>z3F zm$qIlnyubyyJ;Fj=VSnA+M>*7I5>>{qNk^TX?EEvzI1tp7wTT>G!<)U>8q(G1Dv7a z@}Y#UxVe4&4Q$a)uM>}tSssuO@Y_f0O&$glqoH90Lobz|X|k3HuwFlzmu__dy6;sE zwJvROG-vzUp2-o|Q~6PG1GyRDr5sg+x@5{7LNT^vDu_-995P6`V_2P3>aiC#IrGzFEh7a#SQp(R8>e<28^> z>4=Jmm`#i8ubpJcE|E@{$_gU3G1`7##^xs&bY@2tv|}<h4M*WKJ@MWI}DPB;`0tQKl#EScEyrZw=m&0a8}N^LT%dX!+>BZ zhAoE?+WSOjBql*YKqrhmqdswLJK6r#`T6r&&0QJ|^NaJ1HTU56t9!1+6=ms$5nv{O}8(3y=x73cf+t){cCJM*4d7W@ax43Aevx?ZnI5J|x^w$D#&%c?>?=^6Oc zVx;}@iy$af1#Lm$d!s1`1sVhY_T^jfjke6@M`B1wLIed>JO5Cy zytqUSJ21*n1(Pswu;}n7R93#B6|&krOQ@E+iRX0fDkjS);=1{)hzi4cw!3KBPK&BE z)TdRI0Cy0jCeEdE^9wK^o{9x9ks|q&aYWc+(%lx~x5vA+aqvB$i1}&_Ai3udK6lI0 zoe>bX3+#rpO!5S#%PSxBr^2rTs?T<&(=C^$%1l7Wtd!9}a%TMka?+@rycII`Z{sbK z62J8P^e?QcK?I{kf`~9=*EhN z{Hryw?hz1+lo{iLlBT!x?WeJEZoU&hduH8F;_C||jh6pdx~veAn);-LKf)U5X2ca) zT4nYvI*9b&@&5=!4C#N%R)xR3_a&F=Ke+@EFCd!$?BqZ`4pbk;CfR=eQ42oHbO_)u z;+TSg`4xGafQ*EgoVF&Bo(uTuFG+9hq}g{j+z&4k>Vtq88Zb}A1y5wlT!N&MbCWI2 zTmk!&{`U4Oy(^T=!-U?4aK~7vs zCOEL@c?|IHCFgeZ^`ZNJPhos~O9k+=*n9d!5g>HGmSpW$O_D5%^*osIoL9v`H% z0<)u4&;Bq|G&!3?@|AscDj8fQ|R( z+PjbtCLsw|r&Av1GneZ&{eS;)5nKs*oku8HxIy;{PSrKg8~; zPYZ+5;!H5%kaygDpqQOu_Ya%S0PwsdO@C!{fRKSW;gkMIvwY3K7^cGxQ2YBY(TO>| zBbriMyQds06L?*3155-~n=yv(H3o-AL|_A$Xn06nSRGL`kOWrWc|8@tGJ_fTC#045 zM4eZmMBPu|n|1EhI%qGJ(~Xaxrq}qXJ^=sS@7_@y(wlE8*;Bx7F?~22!VmNq@MImE zem^q6kULviy5J&S5rzc#L)Z#%R~BVw8(NHu%gKG+t^!H;<-|2(`I?!zxm->=_knz& zsqw9aM}?_&QfT~8iuHK8H2T__{;TsJ(zpg$s}gXK?BPgq0O_%|x6M(gDgX8@J<6x% zfDU-2X6mi&Hx}-+#?<-*lXP}%=E*VZj<^tYAG?LI)yL()<*Bg3PAmn|jYh{VWBNLV+`Ozt>~+sNVXU8Vwg?Ctf^B3477-+StqvN%X! z+|^2|ebBT>(0q;+|F{1~CM@C`MBwwi7xyTgh!S}9C1W1dvU2v?2Ux)*^0*s(^8vC%Bd*+2I?*Z=7` zLR<#Y?Xr!=yO2kt7N)`#u8+`fD^AE)(#%I+;H8GEpP`M3^wV}*n_H;(tY4HK=~7DC zSbhEx62j+pbdqUnqG|+c7f-eQEjji5`G2?|7lfggtVNi;%k?xu`%UwMu-D!pF1kngW&c+dCR)r^kG5K8nQJ$sr=@94e0mBbTi$3}goOCcj+H1ff*(IN?{eF3 z-coS_WPl4<;1_&|CaWxOWmQ^HYRItB(;*t{A1EOukzq*n*v}`3h?vXaDOzS$7Kbsk zSiztVIoM#OI3+Vv3b1a#Dk$LYR9O?xx7^KZv$#Zo!NCF2f7}$5mGw`)>j31k&n&@I zy|mJ0HTMH*IXgXPy`E{@w;Z2R>WT_pO{T})F1r7Pfr4^%@2+nEyt>@U-^lyyA4ULQ+2dC(vY?q## z0QYa5^En^+>tAIFXtqNN9-yu;59|i)S9N5=X}-`tD zu!#l^(|9(EuW@lKwp&{pJu$p3Q$xQRv%ZVL=KCmtjTVDrTq3GaR7|3#`i+8bFjrYA z>#nI;g}F9l0dIrm-(D5mjSJj`S`EPcl98Edtp5=N^R(I8Z7-F$wX9V_9gMTm(`PDe zYc@hkt?f^F-4~lPBt{mTh;}u6ynHS$wHIzs9S&cgEVqYB3qSjO>y7)e4|u>Y`)?dC zO%<&3E(3dS)XP7D+MsFBP+vFwhVx{Tqc}v&np?yGY_kvw{yj7Eik_Yq9Pd+GCV&l> z)7f4_%j!!kE4NJ!=})ZqizQ5DsFV6G5T<~8^+4+*CnqPWz>3#@6nQIP|I;M1s)<1# z+~;6={o&xn{d#oc{awMp3u}YZjWI985u;e!xFCB>DNNynY*yCQDf)a5M!?MclLsMZ zY6NIMQvO@l<#9$Bq8<41y$LiBw}1W*8*~uAjUj&-(P%dNuO>CkLGmld zzKBE!12j*+Ux0jRzHM%s6}-ipdHr6k8n+j8p3QBVN)0 zUQ@O$u>TWi#$4@DBltgKtCjE%w-+yAxxaR@**@Aoqc4IqaN6IH-+OuooSw2ygK~qX zfq}L*ws=Sa5181Ds&!@wl$0WjS(#w>OUud#*o^U>J?lhzj|3i;aBWIP1J9i!F2h4Dtn+pCH2r?em{jvwY(Q+7-M)#mML5vX%Dlp~)%Y#{ZukCNp2dr>wSt}Lxq9m; zB_@BI0z)=APYq9r$in0^pcY0RpTuEM=P|h1A4w)Y)}@jpc;j$i3Knj}qM)Eqb!W~U z%4GA~P~GNF-CneZlm?hYuB@yKI0uXo7j!j_zlB-yp(Jl6vFqZ{vzlucYSu|#zi1E0 zIsbg40%C+2q!Y4? z@hSI%r>=N}tjwf*#-@V`s%H_Sqge_dl6}-<;g^Jj4484%p(l38=#F$7jGudPpv*77 zezn-x&Z{aj>xWz_5QlDSmO9YPRELbc0HoT0|fx{igeaa1wD97 zmTWP9`tFbr7#Ep9FaA?&;>MkAky7T1g5wkMc?oYuax5W<&3dEesDXaxR_pCcm)oa5h8Hg@9lS zOc0-7o;lT9h9=et!F#jSE{mvam27Vuhwd9!SnOXcET%PbUfCXk&%^|7|b+p3}P0y)W1d*XJTDXw`{?xnIO_Bhyui7 z(l25}_v6sFOn5&(&csyd%33SfN^m(|peAM}*rHxjZjsscwT}D2izn#llcjFGbME%> zpw>6HcIS7jLKZ%Si+=4+D)(m8s;YL`gnYf@Mi<2x^5e$`AqIJ?wsV*5u>yufZrkHa z3S#2jGm^dWqWN?)6Q}RxwcsdR8x3kGt=Na2ZyaKRa}MNWSO8DdQAC~>mAAJ2VDkmw zph!6MvBniF>GJ&YlG$pkMyD1kEH;OEO*@k5#tW?zyb31o@_U!I=s~kB0UH_BpR~C$ zt25ET?CUEwpAfxNq&B4|U~;AarBX&!T0tpKr<0ISINlsax{M0C1KR-|MlvfPGe>^+ z938Jm-?+Nb^c^IU>;)1YR?u;kp1*PueRMUZz00E*9#KF~&s}=%g!x=D0VK?s4`u)K zsi8CDLBoHO{6=@Jd}lKeyXD8*dqDe6eNR=L71w60K%pk00F4c_6{wFL$Z9dBvr6o@ zmCJvaOM4m+Fn`}i7K1UY zfhDf|nGuSxkdRkor+t+C;b6JkJRi*D=gHn^+D0AHOel}#7Hir5Obr#t&e79p;sxa= zyC998l5(s{p&jTFq-08cs^Mf$Okt?N zWPM?wB45#k%NDEwl2=sxY;C!=mht_cyMzA)uV;BT*yTa(FgzVG=a%0g)CH9&5;E9F zXLV%d1+sP{Mg7doDC?}GP%^V~!m(VTT@WdNPR#Q?Q&~pFV&p(4s}BYV)ce!e<+#1u z_Sw#cWTmlU9HLq8|t+4 zP0UPl3d{rL4xPVFPEfaCN2~rGAJNQ_BD%N&Ma0BnOa^eCtQLYHwn-2)am|8mAPu3M zrLsI1+5N~ci|srsEKN{Wwz!}m@B@%?D6xXU(%!oMv#&er(S<}5 z?~GPupUksom{c;$Z3FcRxCB`X*>Y|DOa!R3&t1Ph&h-NPAU~AivRKwvTj-*pNE};l zbR&A-{THouuksD9sTf#(_mywRzCitei6KTh>%jCVu< zTHOlgb)2bd9=)USk~l>rr5E+YXJDr zl!)EdsS;7TX15NCi|g(T%kt`{-d>p81M9x{`Qax4HdDQf8~jd&r1k?UkBaOxgS}(b zhbr#XEiErip3BaDXqnIJPvjETP^25r*TZIn6TCE==BjtT3VI&0Uxkvm%X9zwi5O&^ zf`LIAA`?uYt{!VwUOuB~U2&)@QbL-ZthRWG;_%nu^y!9?_Q=Rcl^r;fpFHt(b9E-2 zYcOaZld{l$M9OCs;A#qo^&aPUX_iF5 zA85!IX+MZ%H6wq#g2d14l66mDQ%piEBdWZyd2nb16&*Rnef$A$Ol=RqQBEwF%qTR0 zp=sOHYSYf$EhAh$j7S1-X2M8Y%HAXvYwV+Hu%HpVs`1E z`gKNdFze~-lkmEQV$-~)y#z{<5vF`S*$Q)FX3DmDAG=bp$0#KCSAOZp=!jvI%ZCNR zMd=%9Cz)V;mi3X>tl0*pFE<)oR@Fbfe_62iLL}Il>g>%_1v&~w+B9*XDDXC@FLH}cvG0+GUSSxE8Sm5MRGrGWCUn#shxU@898%V&G z{!5Ry|NR4GGsbdURgkq@sK8Mi&)p99VUUo_-ZIe5&_k+Km4(|zM`K>MjBGYM zNgSH%KK4!Gx4#oiF5B8!qkBbCcV)Ldsv@G1xA9fY?rh)vMo8k<&zuK=EgdvSCQmRff6)7OPND}l(vOEep%B&j<-Q95zIaKdX4H-hbfp zyS*L$=kpzd{!DvGkx}bf9ZYa;vfLI}*@t)>gHz(;XWDE{*ezzINh?kXZ$Ot+@Nw_X zw00s1+SaJa{w|B-l*P=tqCX}YNc4nrTbj6!|J)hq!OKf$Xcv0U}Ita#A`K;iUk+2!#= zUA5Ep`NYJNygDs9Wp;%ugdj?_!4BIqNut&Dmm9hg90fp7$5C#c)pnaR86_2;`Nv*` z*ZimHy6|D&E;(+!LF1ATNN9#bTi);UB_c|UdfOaYDL8}}Cq?otbHbvtr zhE8v@%o6}I9{l6y{)7M`Z~wUA{j0F~_MiS|$+fH#zzF<8hc;g~^v2fMT?;D% zXF}C`^-?P+9uB+3Vyb(&>9SK>5I%sm8q(5u=`650*2u$MX%N|U?B3afbJOkp{g45f z?!adHJS6N={cefLcFDNflbc|_Adrmbp~Q=Ql2`>)jw?nDWmcck=%+0oXK^e1!{tS?UXx`x$KSKTk6A!pjVk*_p5k=z(UQIV2MQ`1tn2?@71wwRE< zeEjYo2ol|x&5nK#3=CYj!(wVb0VfSh4nG^4*=4wI+4cyZ!$!xC#Wb0uH>R>BnF0Ab zQ06pnoP9cU*yb?1Fq!PCD$&PcK8n4(jAD$}X;rYiCy4LpR9Qj`bmLux5xXbz`UW6) zkpD6Ifi=PfC(alaMZ=)oRTKga$03GYPoH39^b|?ywf&nc*V1AAp7Jt_;kJy~kEQ#EY1YWYvXlTGRTVjQcOUkdaq?i|3FkQB9b9AEi*8Sth zAs`tB^0vWG@Ul;w<6QuXlEk*nUMxQe?Z*Ly3xjdr8kgY-muvNTY|TQj zPFmX=&I*n4k7db8c3%}@GD5_TmCwv9=Bv7C@p=h*8Nmoy`Dqi`7+&LCbYKqk^2h0q zfpfEE^D)^qr+@Vn8Tc&|!_LCS7D2dh?SmmG5>RfcGoEA84?LZ2=R)0;`i$Du?=Tz7 zHhYCp{7e8OcuvgpjZ$}H;t7z{YEl)vC2;tI$cP4eyTLkW5o7?pFs<^y(at~);6|{< zArtbcUk!TV1^30AI)2ai8LRylaD?Fce}W^b(*D2^_22~j8;)pO-CtXq$;>XRr+Z(Wc5iknT4WXrfRTG{>n~lWuWO{QU`g zGUI1u^#^Vxa+C3K{yboTSCvx&AC}3uMY_R2nnwBVN@mPlC+l07r72R-XRaJm-$_*~|krEG+4SH?OxVjQpcs9iOjT zln-TYMIT}U{<++2uGpdOw&*yHZvbzl zz85`K6$CO#ho`3NXSPrg8DM&FHk{wv*~!Pw8!qX8Bzg6Z?*{KE$f84-D7 z4;x%1lJ;~2A8J*du~lQ8u~Q%DigIgu2NSGsF4|qdV!&zU9Y`d|yjg>(0IRs;x;Z$# zaEo+YM9J&So4Q_|q^c#?Jp=(vd$W%&g=eeGFr^X`3eZj^W4U>fFI)aCL~`WZ70mVT5J{O2Dp4_rw%rekx$;TE#=xf7nA>z- zKOI~v10z-GE=bT4(W3`H7$y$ z4+~3-@MhTv@AA>?!`Wk@h=_1QvBWpFvCRhw&gUzuD+RA!Rr&x!WSK2APBVeqHlD>| zF~BGcOd&LMbSzHy8&WrB#x_AQk1En!jnvR9oWw{;0iM_ST1#T3Y+-c5U-t8NngngP zN6aH?PA4a8UevAUh#Jb4=pVLg-aob=Y;W;Fe{Q1>Y6OlM*oFmA)RQ4igJ%-Hhq$8t zeRIT>?DQ_rJ9Gv9!2+r|SlBX%CYthyDFmm&Ir5eKxj@1-sO| zqzffR?A0R7+>z_(pi3^t$@qZ0F?q&W5o*e zt7iahVYirn`*mRa{GBr+hB;vnH67T6hMY(G?!pHGAh+R%@du|&#^Z&pw6jtD5MZ3H z6cShH7UByE9U5PiB=i7K9cxo_a3r^ZLDu(<4zS5UR&S`Y)9nJ{pJSzcB0n@R(82QD zo?{&)ieXS=1D4TEvd5sgp7s4Z*kC#+#iR;kwY^y5V?!e!BB&2+2a3Y77%z^4c~HO> z3w~f3$dn{XGH_5B&a~kNiO^|2$aTp-wHb_Mcf@3pY7c^2h4gBX2`e>awG?FwhDJv- zmiXxweZ0g^JS=G2;MKB$LB1d76^txA$?GSP({ggipp-gQ&h;DhWno}?IwSkl+z{A3 zIKRzasCg4*9mkHkVDMGoF;d3+IGRjKDM&r?C%wO57(716A_W>1v3z>uf$qoU#50bs zb8~Z_%?E{CVLj`l^-uryK4@T3hhS@~3iGL4|HOBK(#68UHhvN@4GC^?a&j_eIWajo zCMK`OCt+CyT4lA4*4DYxE1XnR{n4Bk9CmCYBcb)g8`z|{{q;Uu4{cD;9cvcO`Be(l zYV=!QD>oe#y&WnqSnlCGHhi6(Ty1}j{?`57(70oYS6Why7s$agGBByJJ$6!7%g&Cy z*gdHVw^(P+HOU6*aRt4I>7bRnJR+Y>U$$mdRyqV)wBJ*C`UlDx-tC)p1|79Mw1b%z zf>jrO1es|b9@gFTQ#$cZM;U69F%Qax0N7Rscg#6qp?1AXV1#Y9hrKSqJzIPcU6UR@# zc@o^P^pHUopI_~yDQh1;)-u;qtI8>G`_+=!54f(O-e9~_+bdUec zS$w{b^+-dna!(PTkyatqqWrGhUpB=R8r3coQcteMAg^ZlpFVISba(ZO#CfnR@6OH% zoX4-uzpFhwzyu4wz6@bTg+upR@%+YG+pio21UxC%Jn{mhW0}m)X1q#sm~Ag51zU<9 zy&t?eV#|x{jb^I=v_8dy8#l<${oNh6BCSqQuO!7Gc?~=NKa&a27}Eb zR01xuu9thPOtH~ybWyETFS$x>v+y}>{oGIG*w{vyb%pItHu*jr900K6BZceD}G zQbp>IsX6zjp)8cJ_ulIVd;i zZ{X?pRN%&87$n5I1FQR z+o2W{qp?WZOKZCpub~Bq)!f4ies3!)E5h-D$y!@(*O@U3sKM}MnP1f-Q)fpf)@+rc zx{7`<&0A7MMN0cJ(1t-kE4fT5naK!m4~CuX@e2f|GWONFbSA~{B>Fo$$8M7}pnmrY z((!U3v{o-KW*;ar8cbDED|{Ur=IlBs578z}Hhzvd$BgmUuXinJY$M_R{Iv_7Eo|TR zjVvhs3HkaJdVuoS{KKcl+=zQV=%hS0rssVH5sn9_yRR!OmM>@a-+Codkdqreg8M0% zFkB{vMnxH{(sEI@r36lc{$iwoSeMgcksOG<8-hr?b1`T|_UB66`wO4L)}Wwod;T1A ztVpX>;G5D66^dKFe^j9?+tpH)0%~&n{f9oW9L%(X3W%g>Z{*R?8M5IS2CGG9<$&w8 z=3<|FNaFpyqZB@R9BdUcbq1t)AI|sqMf-V`*BhH!#*`yPJy)0J>aGC8TJYU$tXHGl zxw0x-Wx`&38k#&n%oipNUGyEsi z%JZoBKf_eyY7{kY7ex0$RGuZhf-ahCa`q#+2?Idt3kHCw00JdI(ejUg<)8(Om$#tQ zck2HMr-Ee#O4I=1j&KluKjK>i?%xW;7$9$04AyLJm3FCjfGp+q|Gfun&F$a*=hsDi z*{w4B%f7!~kamsRa$e*3t-Q&N90*7bT;=2j^TzX09Iqk~Qk_Pb_n~)?ToABHdoj~` zxwPXYXe59c+P>7{R&cvO1)`ad;8iY*l3%aS_hw1iT(WCxd2TgrNr;GTm-(RvoLjjb zbQ9=zZ!hzYo)7WY{~4XT{f1lVPggIHp>1=pr%eMjF+dER`}xd2dI-+l$~vhL?*+>e zqVoyX(K*IpxHRZySGEFY9;D$QiN}(ME)J9r5>C6N6Seu27wyHbKbvpAAJzq(xYkwTZlJfFuw~OZGM-}w3 zF(}^l_O?b7$@M=&ej=o`4$m<$fuu>|?>F*%c@GJy-;sRBW`D&n37&!&NupU(gn?33 z^$9VTl?}9*EE1Sn5;-}Ls`L1|8=cFe(>Mbnx520 z+P`cbM5Js8aocPW;Nw$7qdFBPq*-gI$TU6sf{>*(%{9OW5TuLE_GFZ6e^ZMH*!BZ< z0=0IHdP`}jbG{iSPwn#ciL!q-++$@@&gvOSztbia4IZL?TO9Uf#i9jtT1-CU?o^>X zU|z^>;(af(aykD;B}ZVLRmi)~@1T0ZYG7bs0W(06GLztLH)7GZ_DLEvCgycp`aZZk zWxw@~G2usahueKx2<&kEc`UnAKPm~=Yw9=e4+cOlReiQ0DDk1=Kn{qa^}QO<3}dXj zsrt#%9x%8k?bWR@Ky@dwnWg)7BMVfu;(cM){S6y!;(3iSI0fMVgM7o~JTR@ld8Bkg z)y=yV=bi4n=c_pJ#D(~G*QS{)Lw5RDoyt6z$u%Y`>Ri_gKRiTnfkOZ`90{K~B{X6; z`c+{%y}1t`8LJOmP^hw>{|a<_*1KS2u-_c4sVdtAmHfpE@&_+c$|OmvwOt>Anm$PQ zhMvCQduJN+a8@GURlX*6X`<3?a~55LqF) z=Gk(IY^5v2A4>Izd*X6({C7A3$$ov$mt#1^@7OOvChoQ6#KZqdJQsPB^RmAt=xVCk z_38-2-t;(%DPIv`sR(g9HtDq<3LzVd4-GB8`uV&-Nm#gEt}tnMWgtHnNYWYAPKZo_ z5)F|w(~34f+#aX@AnY;Y4XL_xJ)-E=EHY^>dalNN3Vb#|LPjjXCHKl?7SYA(($dRw z4PPeD#|Jc@(Ef(v54J!Vkp8>qJy`v+!4J3=%t6=^XP?2flciPO6aTA@iF#vH{TXPS z>s`*GWG*lJDlAay%_dT(+;n>E{w!CWFnPg?%5hND&R+rC6weQd@(0d~xI{n#%$^Yh1Z(1u)An_TAp{0S=C(#Np+7y7L- z8`Gt2->ER^_pu?$zl{Ngugy6RM$M^5caCNk@pq$`e*idPXYxm1as%s)r1Whm_i<;`BwZsDa`=(xW3Q-K;{7)0mT{d3 zAjki~e4AO#_$#4&(9;c25xE|&sE+NN9a3l^7UZl->iNrv4u%$NT_1K!OZ)hs>=Mfq z-ktc9pak*C@%LC1!2y4Y-s~yI#9e_HG>=X{#C$-$HP})cgy;D#J}QG(F0W`&&eO%U zGCDLWQ&W&?p6A!|rE0IXbIpxZul+#(JzB=3InNT{gWI2Zlr103hGl?y%0*Q*Q_ittj?;j74*%23F05H zMDsrRH$;FZ1H+?)Y(vu?$6QJ(j=mwQC6U}~oyQ89uUQK`k%q}3jt3_|2!=~=94ph; z{Z3N$^n4LJ|CTwU)UThSbs5+qAEr4y;zwl53{+ogcHj7o`3GwN%Pc^6T~*1r8}J$h zFbGudfc&()G9ghB=>&lwEAJ#w;ewF)TbRHN)c;$c7Q(Ej)cB^X>|xiHhF02F0kt`k z%>~LQf!sOoBOmQA280fb1}Pg77#1juaSjDL8d6gih;umoVJ)^g2RhJL}pzKn~ z4z<`dD9hz>wa>Cu^017H*YiOGjacsIucQ5RAIlls;{`z8%fW%eA=L}otn+e|B%O?haRgq2SnSW;pL(yggNmvlY5Ii;K6ek$*XZibYQ@9YN1L!>cPVu^ue8ifO%=Ti z*O$;In3t9l_%-*AC7fKGlvE9uQ9uCmPDbW3!w{G{t_X1)EogR-I`-H(A-renn9<%B}@JtC&uswz z36Q#D_^)trokoEPSxJ~HpjiV3wIUhmts8%L0e>iXM9wX~imaTRZW~7t6*6!a`}Ik# z7q7&`pzznBu_vfV{Tk@5dV?;96FT_s=7NZA3782tHJwbZU(?g-H>Q@DEwvN}KCbRX zP8rftQc+%9Bx5DP@-xs&G7lM({HwA9?I*d3ShRs!c7FHMaIW&DVzifTHcam1os~bpnDU!?~j~lg4{x zsQcy@sGFt_@`1)mqpZvxUyCR-N9S*3Z>&AN8t7v`giiYY3lCh@m>gSQUv+WX_=#ru zuxCsYvOG7FM83A$^*MXx$uFg^_sNSa7T5H249hmaB@_a(0v5w2pF?JKX<6Ari%$fv zdj8GYBl5cchqZ_OewIVF+Lzm4RV?x*u%!$F`aAokF3^Oorh_)wyGsh#*;-83%?^*3 z0%hSauj`C=cQ>peN&Eq+-q2u;)R7Jl7Rtr41>_*|&cW_js8h$$6l z0`^}aXDS+}4flYMy8PqThJ=!bxt`JL+JF%4Spz!e%97Qp(Lh5lgyICaH-08DAF=%C z%W|bicPVPEtn9SOmxQ~1P{qy z{gliQA&R7zzO=4rR8{p#&Xd4v?MbTrG2;igYn+_ty-r46W#yb|g<9P1O9>#nvXz#` z&B^;i-KWK|k)YP`nOYEd(GYR^862A%x4P;`c`U1)&mP?V+)1g5m}I_zRQ}II0>Mqz zOu?T2?5E$C+x_mJ_3;bdoT)A^8ahm21s#;2`-qyRCV!vx5Rq4f2`-s{)_GTuvJ?1L zd@kpI`Th_;vbuMq#AVcy!$ssS*U=TdA^k4f?Y+AE7s#bnSWHY65Cu0TZlfBUc2|Jb zR3|($i1_o}yZ0Yl`|-iD{upy3N2#l|8K!#m07}_tuLFF-J`dX4LpyZGWqO#L01BM) z0vxvIr@o%HgTK-7=db<hdN7>2xTN_X#kV6{^4FuUCAzO$u0{HPe*Pb5>{1SZ zuL#h*{hlZgCie9E(KZjr_=LOk-jTV4`D8kEZY`3&MnN?64194a#Fg7q=$$>IMSoSY z9-3)zRV5|rKx$S`{Jx{!^C~I+E1+zZFnpg8JG;Zzz;k5; z$o0=J4wzv_j~RVR3Saum0q6n#H}NhxQWfWfB+=Z=_#Z3)l(iAv7ppmgsf%@v7p($` zEuMY-{F~>o;UsG${Ek~jz^ww~57zN-a6n}cs8Y%}xrkqn_t$=#k=n?T2f^zrkD6Xk zdoC0{1U=b%PM(l&fq+9vX>qWkkY{#nuE6kuTB@P%QoLiq5IA03nlH5GPbzN}m0OqU zAvN9@13Pg5!~+HQ23aED28ntqE;ot>fI9b$pgqw!n@Rg!hR8bCqoh-~GP`A~1&J_e zv?Evok`fZLVsF8dLMJlYG)Ty#1J*?IqqMebYT%uB@1$QYIVhsx+Gn!K5YJRcJG;&e zXKYhRpqy8P@ccafkac%d8t`wE1>8r*d)8Nnj2q{Pd35(cc<*~F!$>3GkNFhc+|q`9 zXJw>x8{iHuXyT80R>;9#HYTPZKAy``U!C5juCpm$y^FUO0Hc6+NB)5*PJ`l2_u|yY z`!4`Pue!JFz(HdJY?{-qXab>?gTHZB(~ohXz}l{e;tQ6l@Yj$MlY!3Mw7xhf>0cK> zYs6vP2j&!F*6|HdFSC-2`);jFgX_)%?tFIJ2|!C@e_P2mUDM= z0k>`_Q$^h2v0AX^Rrc+H{P9GVRgJ@jAVvQd;=A?9P%pq&`ggl$8m`Q`uqAi~(hRD0 zw~>>T7JEmY6^oP3hJ1V)n%o7bQm3o_=eGe<$4+w=&|Cp=-B_0V!hYaf&C4sk(qj3J zPO08F(DDK9gEP(iY?HXXN4&?v&zZo$zzfU=1Z=;6569F6;sW#N>8mRDbsE6zTZ{c4 z@PaNgiL{}|2hKmSXKjUC$v;|I5dx6V8*-VzZ5Xz_I=@D; z26_y9@}&j%5Y<;)XPfOM;3D4KLTz$B>9_$K)hIPP0o$OjA8@JqFS49b<3(~KL?H** zE9z!MdKx~`Fw))@ryi-c13i=w}kXdz@%9DhsEuXT%R z5qyQ@ocqgU$E_RB%&ItJVmrEeJEEc^bs-ZW0w#am_|nnU0c>gq2a) zz0GzPpye^6p5kVHg=vT<;wZixFRIGcf~2Jnyu znFFN5g_U+_jkrM73hsmcom8I@meUOW|V6V!8}pozr=Ydj#r3= zjWIOJRBh@_p!XEzYtnTGeZwD~hM{a};x<78R9_XUVE0i~3c@+xWfITLW!&9ss`)A( z-B0+(X(R^(?vi<7k=a+XUL7)&y~9Wpbaj{~*_@h|P2$7D$A9b0LicK6;9ArBo)IJw z;NsAqba*LOqk(lNK*lCC;vDR4tz1Ec&cUJN(w<_}!!3KxrKhXwv9hD^OQ{o4vhsAS zQjW!J0rFvVB=cI5r=_Vb@%=CoOf&k;^r~sUCGoi*0!5~`XlNT4&SR4upZf-YX4alU zZh~MiTlcnVdRT!vAf+A97nyE~$Y~W@00Zua#NM3k$8A;yT^)ZcgeFMLSbSzi^E29V ze8{J3UX@vKL0dP#%~opp9I0htk(bi1pmfzx9US~!LE%zkTuDJSIM9D?BJA<`HY3PB zGVAxe_L-Z>epYJLlnL^ff#5w2@^s(QW;r!Iy_qXL{l!p#Z7QGz7Nlon-oeBia)fl@ zd~rZcbcnLvz!;b+mT<1Hhs>`_dF>3FqN?qUc7d+B6Qmb-w>%Y{_*0 z>3Bag5CLtG>&YB4VwD4IcT&_O4EGglTn`rzB8UJ^#~lkEmva%o-vrt^kjsv(t$1@& z*kmPgoyP^e;jz=N;8Igf>{%LLQQFkl*wFLwuTiQeAzzxWBES7%Z!u*YSA9D4(W-Zx z2O{7uLY5p<3ONA;*qc;&bas4~_#oGUFGL)MCHWsd7{74ueZjv)E1gu~h}>K1>7n-m z4k=OYiAI~>zJ2@BEJEEC+T2O!3sohLnOa8sU2G*=7 zhOek6)4q-UB6mOhiHVh2h5e|N--zSV5)F{BSs3*~a{7Cai>>+mv$N;YpTMflF~h?8 zdIHq22ZEknVH`|BX>D?Z`eBR51gZU`fE|3e5PQe_}0F8NObk+4o%>ZfDK7TyE> zl|{z${kNs1r6GyO+x2ab3l_Tcm$dLOpp~O=%}@eyOf(61+7>c@9Ry0N7-Fbi7q`kx zW(<;hd+cwvg%l}sPDf)NMMuz~MHFpK(Vd1v#=Wb<<;HGvp2Wxh=N=Rx8ffs3d~WTJ ze9l8~#k+(M-5*r3vD|y*LjtgKBSTjGHIWI)C66~&Ct(l{X91$4Lo{hT`q=QbvP0m< zqC=e|(5co>TsSGbI3l58j*gISm zGMg$4BIS8}UoI2@uO?surm6YwS)I5U^GgxSxMY0YlD=~*F>F-)rw$aXpN=Qc@gzJJ z?dT!I*uPm;^a9Yeyx;Upn(s+hM$1eFByA!GfQEbE%AgY4i|yBlQoO7z_%$JP-d(ukBW6swNBg3x?I5 zCaS6xUqsNg1?)Fo!}GPzc$^4`JH6&W;Frl#nkr^KJieR; zk@>cpq&HMTuVc(dLIfE9Wci5P1X($Mn$G{-82Znf|6k3t|6hFA4&-p_cER`3klV+% zb+_rgpJCmVeT}bdV?E_+ov)%S$}VHPk&Z5%=rN`SBAyeQm?@ZwRDi7Y~P= zcGck}uS|~RS@7~Q5gDH|+jDwf74J@R4`<5>=Y{ZhG5%@9nhLwM>~0<$WH(Z@K=9X3 zS~?ol_YFDPlZ(r|9K^~AQ&VnPaY*Fw@HLo`eVqg*_gcf{6h_@us8t&p6SJa~JF@af z*!5_wR}c!^a|y2U+zB6oD6Wy_+}*>Tbq6T4A%^t?hKg)#Y-EUvLa&V^v3le=mD22? zUn&J2SCih3MhK6{p$ZgKf_$=-Jkl$Axoq(Jy-87YjrGU;*@StDt&#C8krs zAgfeel$ZrTl-|7gMMKlNIZ=_sr5~4+q_bK%R^>c7ZcjvU0s)bB_D=Q}i_7EWIMasp zb@r7)ATR!4W9oxvmL?G@*j$YX57%rFvVN&m7Qsh;21#XrRbBYS4$*q>7BruI6&Ja@ zT%HpTAzvoK=S1(FoAyfcPnr#HIW0~^S@Kf|9?Hqc2xpQD>NKd!$q1qP0 zzEqAVD992QY$I`$c3@5N^77Ddxsw6UQC-TfBbB<7qi@X2&p$C*prUlJ*J$Y)g1e5A z5!YNt@s`fp)bq*O?n;dFRu`bnxU!W3%#SxG2c*RTncNO#UzogZq_7VxWISCfc90UK}?YkX#Y=kk|&XG*XtA zb9Ut^&bZ<~#D7df$6TGl5=zB_X7# zrRBDsFUL=s`#4uA^mP%mulQHJJNm#OyHD*A8}2zkgjeMC?#kVI`?itN#D%mP zX(`|KJ=1$6{JDFrO`lG~{hNw8br^hzh2<1GEG2ji1J#+I>B~xVVf<%G-O@QOZ}Y z?Sfu$1b)@@D39dOSYEs1AAlAYe70x4mZgGCAuf*T5)&UU?!_v=UIOh}0+e@M-NNkM z-g-#PT@tzvUg>^zB}F3NLdl*Z6@uX9tp=?f5;?$Yv(p}KeA8oD*2Kg_I_dOj!?&x& zDmJq^nzj6C{b$r!!1FmPn9HCjcs`F@qSMGWaI6W|6f~9$ z!%hCHe9zRv+B`8V>P`JUfB*Y92c}=>Ij5@jV@fE4@)v9jaFVH##@OX@>Ri^}$Xe<2 z_2$#x9;(sRvoJB&r}p4pg5Jvb6`UU*8z1|EIt#x4Niq77;f4%<$iepbK>zD1wZ(RN zNZdn|V-M1KO`-!1*3^rPe$sOD@#f5&jA7*Wnc$@yzg!y#wjr$_Nks-6HQm8IhR%fK z38)rMzdIVr^x(2+f9>`9_sH1t@TxujLiXT0ub}niEDEEgWg>aZAWyu$R^@vM0;^J1 z%7*oBzz;gB_b$H%_YpaV*{AP38E-$BWV5bosKDQb%Q4}S3xtu}b=sH`j_jKetYy+X zrxlczmF2bHY7Et zH1R}AmR4$hz61tYLDlj{YpGQ>cADUvfW(PU0e)iC;U$CtazEKM+y>XF5_@2D#I zyq@dg-%W1#v^c98{g{T!ZvV^oJ%(wenKZ92zT%}<%1H(ft~N^zr8gSp_ZjTuw|nu0 zVolsrpu+4AAIB*&h$;B_R^P)L8Bc#nC8(YF*e9b^uQhwEU1qgbNhyshJW8sKdqySq zE+#b;3ZvY=5WiU_Hn7abK-1gSRe$iq#96^I$g12-71y0WW?f(tor3({`Xk3TVrwD9 zY*T})>Vbw1Cz9TD_24;dQOLzcuG69n*V{;YRjuQ6WkBfLpy>|@&2b>6n~mba%O5-o zPKe}l8Bt+cWyceUHfrl2XQ)#1 zozzEIXRsR-Jl!9bS)N?9h|t7AA>9c=E(@$D!f9CKhA$=_ERheY8o7wkjP^YJAu)_Z znquqhsQ^w(S&xi@A9oyHxMHn?iGoY}+>_Un7Z@0gJJPp7gxmA!D#rq4#&R|m7SuZd zNwHHcaamcNmE3Ik?>Ot-c@`v|x83V2=ixM4QT{VsKYydhSC~rd?W6+pUzX6sYWkrZ z%t4U#iKwVUgX8*5+u96YdZqiwfWZ4G>B8br-2#(}ArmE2*%BUx>jb48sFtFO5uspN z38(&|!cw!H2|cI-Y9AyKRz|$@Iq0A1ja&PuoSj}b&^f6Dz|&pF9)W4PM$1XpKtff< z?ba#V5Hf}zLN0q+Bgg%ePx^C*JmdTW;1*J731%F2;#@qHyB7u18EmPY@o@Mfrwx;P zbLgIa`Q)6$;n|IkeJcDPzKSXq8QXy-;S_=`At{mZrTYu!8+@B+CIZzR5eEm!sQyLw z1A~_-gN&q50S`jyYC&qQw3suqa51@~9^=^7tQ7erH41R*-@ZQe$5ru0_+a{adwctE z|CXoY?Tgg>{F;(3%8-q5?N596(XVYAw>!%rgv`Z7tjU)>2DIXS)eV=jCz8L82BWgW zYzayOrDs*uUZ-?#%36`NhLBDpEX@n3xwwKlV-Sh_y=SHs2FYE~FV9c>Vt+knc|akE zaJovp7Hd9>K-;bT4wik%z@WFC8W&UqL&BCkXE+|X#zMm*Zk(0HBR(c_UyxA)sd6Ad~7n;{h%24gPA!mdO* z>IMaI@p$$-qfChRFi;>pHTH|@5qNR-^5sH+@YB!@=AbYM$HRtbo%Bk&b%UHMEkCif z7fN5!K8cybO_WPbm6Es-GBSa84mR35<7{^dD5?4JG}c-k13uDq(t#w6reKRQ$mUFNp!#S zFRZUAOKoqyAiJgVtVK?NE5=IN*33+q5c~dvTDN%}a|$^ceJG48LBspQSEY=aW+gq& z%4(``)BJvd7TO$VMK_p4nhUg#*;2B8zad=YP51U`J)w+XcEP*b;~)smwHD(0m^~U| z>ij*oHo@t5B~oYH#6Rf{!UUD`y&8UR^ZOWLdsCKD>&?9z%A_8SfZa_d;AXM2!!~7! zp1=_j>LD2}jE;>(rXBrMw_j2mOY=aG@x*P>$~@G0Z8#LQiEp$rQlQVX#>67`o>oEM zjC(`{gsWtIB6<01T>X>8eFpB!`Zl@g}Nz8he4_y91<_hO4Kgq#n%VKppvt>Xl05c;B+JD zJu3BtbxX)W*OAFa+vxbpDhA-hOJms|&HEbj%q%4N-aMNnmbgfL0k!QTB3`ozwX~pt zWtOHyvW*zMH9mfU9o)Gv9fiT>04E&t^B!iIfc>-_ffGy{a*@&v$X_g7NO-!C)7^2>} zJifFO3U#Tjo&Ks(OJ*>X#ez$U9UcLD>DS0MS%%ypyXBpcHBaY}&9B|9ck)X1A9)yc$82=ce_l7F` zc^t$jvo%gRaDF$PnTiGr0+VLMe{>ONrp`B^(JFRFAPF|w80yQe0vZ;)FvV>lmsW#3 z+4QJ1!Jh1x(R(GT*38O7kJFFao@&#(MGB)qv!}Gh&c<-EJ35N&>0*3XXkGR*XHr9n zB%RX0r8S;DUnhTUXqhEuheRWG$}mWhZd<(Ypi{M(1;%S?X((~#ZgN3%M9+6 zAbF6a6%g>|r;PaC>7xvvEO(#x_LZ^%I*^62dqm3`^KD?VFZ!NtWjJ*jp{)rYuDJR+ z3qsd!-*(552`WG&$~y4bpKhG6(8_EM)n-K-r_~;j7wyHLBfq4lM*zc?tw~Rljg?3H z9%sU4)`|QTs8ap?PUL!pB}zv0v0oe)Kh&a7eJvs>Mb&!f+{r{f$DP8KuHFxI2}z+_T3v9f#0jLR z-}FeHZi8c!5i?{oPhGz^Z<~{|^HS1cbvmY#9~6n1Ygj70bVjTg^0D3(PNo6LjigF} z!Z?#}g|3EA%!$>j|UeJs5k5 zynCSCXzy)WUp3-YES?R@2 zZ>8!?^+!2~tQW8gJLnC0Pd{d)I74{V>7#J^Zy=e0anp%t6N?!9oTIO2KxJT4>W=D%( z)Qk5%Azm4}^|Di`DF3LjkTk3CuYb@E*YQXmP}%mTnBM4 z@V5?$OK4iTsVuH+0rQP5p1TNuywdVp0Mfcg$$fo2%ah~p**{lqK^~-xjT9K-WwJ|3 z+qu0hK?S*nVG4Q=@_8Kk>&w*EWH6)YF;(;`H~1zBjWKs)J+?Q_7c~zZxs~4-ZeuZ$ z-f(LL@<_FK$4*ll@n9RjMX8z6EAreAG-M!_S$;K}5C1WVLY6@MXfhu`!a|>zq9n<5 z!qd*^wp&Sl8X7SOdy@1{ZYNV>mvsQmuop@L;SmAGwP;bf;CJ-ws5Qh)zA{QiMTMRu z_xB4=?(+gvKyZ*XGEX74;SGc7QR)ydTC|L7=(3$o7(NKx7q?dOgMWXd64`~`0)!(W zz%c*(@Lv$`qMxwXHB<}&nDoG~;Lof79oG5hO=Wx_8@vAb><_%V|Gbj_uW%CvNuQga z2geZZu7L3Z=0HzaRZES)p!Q<6$?xwZ2{OPF>hvB=DJZZ4nndC{=xifK98){67Qb;L z@-J|Wbl5pK71pREufO7+b87ybQ}6&7u}5G8<40nZUgOd!dmtX<&;R@fg8lRA`fL5n znw_b46?8q!Q_7MBY3gU_^-8ysy}3C(Gl=6xOh^ds+3&Y+-%3l3vkVq!^RIdFG0aD_ zN2lsKK7e{0uM%~%0xgvm!bLkY8W`0(t_~E6<(~rii`7u-`uIK(yx6kMN~?_@qJy`X{CIXonmxq(q8)+_==hcys zD&Jc#FV0Fk4Xmt|oK7YZYmeeTaTzThj9p2HiHRKt7!PkcT>^C=pe0#?tPi?c;72rG zYTOsgu^exWCspF1WcBh4rNG1Bizq)8kqBPsA*|EkEWewW@b3QST|g7)6je+ zfuFwGD2THD4i3Y`O*- zKraSBzgEjFxl*IdNk@0+jxd<-C_=5q-)m|H)1K+bOP%|O521%2oK zlb_I`(vy=DwL%?1=e-vhuW}}Ud4s|XDVyG3mX?;1UX^U`cxk`s z^HIxZ=2hh|x&-yrr`hh}7VN7^-T4tI54Ebp01q|p0&VB3k;wicwYK17Skx+q2P-@0 zD_euB0Uy`lVI=*f!u-h7S`*a|0U7=Cmr1wEAi+#m5KOx9UAaj6r{Y#J3e~!h>wpiGc&WqCFL8t{gN~u zUbq=DN2O3FQPK_n0YF^=A6&Fg(G~ptx2CFvr@?VX#^;dj<#iJZsuXtZj~|~2)_GoC z7VEcRVPVxcZOe+jb#X)?+<~!G+NdH=#;Z>2(9qBf)b&ybTYf0ya**u+QRYa0ORd5J zkN5XOua9PtWfvAwP*5~D?=O{=mBn$HD`F~17w-&9%yxudouRWf`H%kctNY=02n;uF zpB*7bN7J_6+&cIh0&GaXqqF_(*D-Rck34-=GM-98xK|KP#a zn&WR+Ujrqds-CEjVcdcj(guN6Sw~5!RMs+jY_t&G$Fa;DNX(Ux1WOGf<38S+KI5Fb za|*VclVm>kV6za=RLNehNv+L-0NOOM`&?|?nOpF1+UaUn2!qD^^zx#j9jlQqe0lk~ zir<0=BJr*_at6ZdCpt}8%rGM&0$*JaptnkEziXTY$dmiz-n~nC^?d7BclS#L2P9tS z$Kf8XK`FXMVDu~#$GPf9;ql6D;jUqY=}@Mxudl7UBKv|~QevXEuI}?uom(nYO4OVd zV?_=SPWFg2@!un)uLgm z@Aei3@=Y4>O!VRs67qiEs8%1Y(C6Xz<8vV8)6@DSlD?pj2Jl)3YYPkI)AFSy>m`Pe zES+YSxl>Lq*YH^x8RANhjH-hXFpEE7*ulP2k{{3 zRzMcAR#Z~*7*ncK!PeGR%N49+Zp(>--@k4%h=N4Wb$hVSh3OVT*GD!I``ZJ_1vTvC zBOvhUo*v7NFzSO$_tS&a&h)t~dVdh#@=jCJe1GI=T%2`5h-i83@G$4@U#+w+*-^)f~5_7zaSwroWe$P_%OGv7=VK-72uz zbZ{tZhuN>m7iSqRT7Q{zb zK=UV8*Y(xa)%EpKJJ{$Yo9E1)iaCJItD*pPX@lQ}j)y0@29S)EdRl8d>T9qb1U0hO zyDhFkry|IOT)e+N8}}&=4c$pcJgtTEGc$Xvd@J^P)0gqU|I`9ifCi+RSh{GdX+*9P z<}8a%cN~XPKz8U%*L1m8@e1nzqTbbFx;UT~zi0n95vuR4vEUTLx_ftJs(L@=jZd~a*p%_3vdPrs zDNcWYQ4fWfBq+Vq&sf?2H^Jy!>)gws8V`N~ZqjU9( z3hZ7WzDPCyy)<6-asTD{#i(<`#Y|e?PcZFNS09%m1Ox)V7SD^zp}G?sfw+M{5`Gtv z<={=FnW&pm?<)*uL+m}*XIx)fSR?>8*N;*}G|AEA$PET&2JV5z?)STwEfi!0!h1;!Z{36- zJjvF^`W>A6rMmUob35WtpCume9df#+!}{Gq)h}6G^Keq5J8)Hb*nJ4?WJ41=41R9jtx|*%~v?+;!RY z=R3dsmRY;DOX!t{!T=aCz85`vAAXx+aA{hcqttE;lZc$abzzwL&W#=TAF_ZYpF_s0 zH#|;q$&QINxWsISU=ESDr>?1Hxiyqm5x|lA3?#sqjKJJM3Yx+EH9VX?AYzN#f+xjO z**XJqP5GTt$tD%2uy5b;d7@0iC{;@0$h~2|OD`)cW>oQt6f|glHLDfwsPBwC+xsV? zdFP)*{xa(WQ&ni){IH}4?(8F=ohG8}tS-H7C zjSq%~+Vh#g)9ed4Aasdft8wFSR@Bj%Ne;N2DKcFT)^>GuSRc%T6A3OlovitLjwrgZ zgA+Px93@v-#F}-e0VP$u8p@JE*bHnVbH7C~xh-A3GG9ul297u;AVs>+e!1$^C~OAt zzh*c{JG8G@H?UYtmz*3jtzRSytdHb2fITUs)Ocgsa|Np9c2tnKe95V)DaE*5FaX*eTG_`&NyGvv5Q07+7orgm(3^{%_lV(5j9i?dn!tSc@3H^vvA{il zsdvnTckS%@) z?B(}O!6b4?wHnAy%9oThw6v9}=80w_D$F|d;jxjM8zv|i+$Q<0qU1zcR$rRl+<}9G z3CZ*tZ!|Bj@(=>r2cbOWB3J~rk9W{(>H8>WMkukZeSDNc*Oe?{q0SPOGroCtIZ(Wd zf#J)Qqn1`V)c-qizZMvp7gkW!&jXUsse`$(sg=u>?Q1I?A)b47{n&|#8&qlY>B0Nz z4e@2K>I@B9B8D9prS{ubMwc>M63Q9%fj~bOoID+L9U!qJ<>s%&OA#x z@0Td)N-<|2JO3wt#Dpbm7ag@mnQjmcl}AZvo{wNKG>b} zYzs{kx?&37uCJ?e%YnC2zfdV=!P>l~RSexQCI2CS1+ylA@x2s=ic;biP#J6El2QtMp{hsc;m;Nds zmM`VGiJ6(A$%)~mxAr7A`z)c{iwq%JT3}zIX59Co&VKt>WI$G~h6??~mYa}gQ>G;# z988xRqYe{YwMd64I8(L@ug>=8GP}WqdEC<=&L^ZUIZlbkm96E0|(+3ftv%4=NHx6NKQAdFDCcz9$)$aA2t zx!IuOLxWN#3A^5g{uM1#{8^d#kiXGO!@8k1Tt)T>KDq)r$)kunnD@ya5j^FXpM)zR znE6RD|MC;^Jjqegv`*p+saP^4slFlXn=+$4p%D{5<-8LyhlBC`ATV!;s8C59s=03b zhNVxZW;|Ca{;_b{hWM&?#RSDM<^av7Bia0gI)CmF*V77Jtd#bF6svsh9zpss`Hw58 zwA+@zUcssSYa))N%8=1fbyww`!;SZpBJ|AkyqinYZIKZX8+E+)?4Y@qq%S($m z8_3w1#_dzViIS|}BAXy4Ge3?C_Q`Q+FleX!Nlv7~=TwQ@MfpVb`=Ttan(I+Af+kj4 z_2|zq63g+fWU{!)h&;XuDNw2R6~9uG=V7sAwW7(sFX_sI7h8@@1DJKzSNL8GfSL2m4^vIFDHH@5F{|3 zu!g&x8_$moKfShjZ=T#*CS8bfb~qX+r6F(Jx0WhAEUVNT>ZJm)1nI=0f@Hjl;@G2= z+V;>S7e%|AylRnJ29tKJ%LcoBkEh0%1@!heJ1t?%LNCz%P*O*BM|Z3gy>j zd%?@=yESS(C3!T|khnMMebV+|Ghg>ZqNJm7ulB*6qkG~q=ue(YoavyYd=DJ-7+48ah)7}_Na$Htg&p{ zdTd>3hIXmRiryHj@mli-kyrR_rtEHGzp;&j72ZVFY(v~cjz$Zn(>YBnwV`bzXk~|| z5J!Bj0#upGNJ`ZZ4ZSQb8GG2^Z`@LZ(hHohXkCRMt#!reuiT1&*>w*mWNso?Emx9B zTKUl&w;TL#euJ`4=hGyd^$cT!K{2!MsRND-(Q;FyRq|S?STPA|ToKtV9a(AlDh(BQ zJm-Me;!n8yU4qjFJkO89-o8`G;csa!b|N5rBy-y~TUUzoLWZPTvaGFa&r~{o0uVH^ z{h|wJy2%)Z)K%EB(#7|K1~Gal{oNdrV_qaElo`rJ(lNv-grz=_-cRlB&}}qS6N#i- zOBpZ7I7xQTEQt!$=3-RO;NGpv_#u7o2ZO?a9rH~^svurbt+fM8 zUs(!Y)2Jk*=F!lUmI*YmFm1^sTKmRep_~}to|w{peX97UZK1VSo-^6z?A7~!o7GWq zDQ5{%{iyL#IWO%w2eFBz1&s!;>CsdqCRdrK?D-H{$&kA%JB#@%a zx?#=fFW}ZPO-{Ct$PIG}3+0Z+#~aOg<&W<@@L)R{MOAy;Io#d{MXB84-deAuAq zI!IdG@iEmkl2j7siG%U=a#29slM=!*bJNloE1NqLh3P!r-4yG6B?ZeF4Jdk75U)r* zZH;pBrhG`y|H3s*rDd%BH-xF$`3|gx%#ZLU(*e&Av(ux9@!L2j6Ptbxu9KIUFEqge zml~Tz$}|P|v&1X^2Wf8|R%N&Li=rqf2&jOB0*cZl-QC??(hbs`5()y+CEeZK-QC^Y z-ON2E?^@qEYhQbxv%hQ4|6o4ziSdlM$9?}|xBSGM{G4lDZP;qkC2ps^HJrsoC04*< zk{z4S^@+X#Vy5(+bV~V8JFGnx2Nky0s?vWl>^>j&7^MOIM-FaK&=B!w_4|X5XKSs; zBH|Ig$kXc03CD%r_~#GCm~{Wfq9~!&^lw&UY2H*_&x1K1QFn4RKq}+7kLv~^N8!6( z3Gls6_A?Sr&S$HQff@;c=9-N{jx_i*#tVgwKvpfwe(-=}L7YI-;q)T@?Ck7ae|?O0 zmGuizI!euwqfI=0IKMh3moujNUXXbaf#;dYNaIlhcvokoegP#EN0L}(Z!fa>Sp_mj zc42c|uu16Xm+|jO={nQbh=DFZ6l;t_uVIcx zSU4wl2G6g;VA94X=y{{wD2kq}^47qIq~W)S^SdiA!erOo41kT5Q+f3YSuX_zV?34^ zUL`ip_{afmKTCrDoK8W)szdqhbIuxoz;S77Iv7l7?Ca?>%i4x>EPjTwiJA*;DmS>VRdDfMtbu`Xa0fD@ zj}VJId&nn{wlRB+6U$`U)s!LbtvbHi@jK}joouo%`dnwQ;RoRAUCo6iyis{L63Mq*|UXSg)vk*I#G&dl1;?bM0 z1kD3-gG2O}99`qPOF2qpjPb}qwfjqd_mmVJVCI=wSZKe0(+`;Al2Z+~Vf9Y6WAEM1&_`%6*? z*#TE%Ju0^ncS^224gVK@p%Z+#!5E*S@iHzUL8hMGW{Lf}R4W794!xqQ)4Q{iMLQ_0 zgWVv7`0bnXnn>-agCu1m`SzB!uA)Dh0QH~}67?l>#EIfn$|EMC;O5Xj^NNNvP=jZC z>=96b5w#tCl#{u^{i!eqxcVum*)zi?k6Ci;+Mj|LGu>HYu!{x_o!LlO`Qh@nNO7)8 z(&WE$0V>EB(#6NZBt~Loyr1YPc;(z>5mAAFI2MlJt=1QmMi)HWcpTg-cuP-zeN!7# z02m-y(yP1wIs7!ePhg#uRR1K+mGgU!V(!gZ_4mcs{AveVYRbmrdAL~jv7$B)PnzXy z*0ZDLxN_8UH^x;_s^g{5uu((37bWO(6#)_`mEDF;m3WXA#zXSIU=`+IlOtZw*7+Nk zGRS4V_6B=vSGIZ&kmU$FI_`lT2>YojEG$2Y%+Aj2HeV(!uxo)>jf`>BJIhoT)-|>s ztn8J6R7DljvOrvM$naPp(FoTU=Opnmpy~S?li_E*-#@5{YLkvX`r`F&p>{AkW8J8Y ziT9_V;<-K1R!V(tx6<{nh(YcI%yvE{F4pLs#?WSr=c=CURY~B??9Y}*Y%D*(`hn7& z;enB1&n@=Y#K=s!+3Lu8?j6|iTkXRUdZ-hQc`4WhMk3bzt5d+B#soa z**n!tBZM1|(1fBqC^VsO(VLEL=y0mzcB8(tOk7g6)rRc8)d`t?U!%^y&hk=vob=tk zD!u%X{Go@j{X9!~%GC9eWm05hMo6-oNupKwGts2FwRLF-z!~?@U%bt1DnBz9D=!RW zMUmys=9fj;5f4eGw<>f#ILRjh@8mAqI9sZRl$e;!`j{okDE>o^?CS^@g=0NA6?Kfx z)XaP2_&Omh-wl}(Vsq-@!EaLdD?tur1=`%yQyo2_5q@Kqz(h6zJC39^IslnOQbOEh zjqrX>T7{9N{J6p8UPDC1H!?$D(~hHz#(sYY>}KjkCl93d(#-wK^YDi#VQ$J2Slj~< zTI7r?D$9$@L$K(Le8D~`;#UC;_B&kHi;=(;Z7t6Non8GBjggtmW1n362{Trqa_(5# z6dV^$R1&PLFP=#bi^HWd!6|Zjn^cP>m8Hot%pmY;8&=3IsPOHWP337* zWWrb!wIhBC3j19AH(DDuY1|&cLh9;BMt^wESq?^Mz+fx3{Dpi`0W@hvpoZd7T5Q-g zq7UCb!ZZnn$0k@lY?%?V;FqcQ5?Vna3sQQ}sr3aE3;`erC;z43EJY0pBLBB(+Vo}^ zuIm@iysp~H{?^sa)jj2C{0Tj2Q>Ga2kIdXukQmg_)5;dO3O(Q|FD4Fi*f)_&s*i&K$XDyVNenkOG3$nYw`48IJv1?c|A{2R z6?(@#_(Y7FqNtOOkBRt~v9Yn!)z^DEtrJ8ZC0+ewP4bVZ2DxXF>g9y zvw~K%)2HrunW29`>$v}a#JI=99`o;VHOr|T%#*gMs)kc!(5f7a2rMV-6PO6|+ejpv z!<}JoXd8Rba090>=;!g=E+Rmo^kS`BwmUK#7=%SL8?-3OetDk=?{`&ZN>siH#BaN? z9mGucj-*$y8I2y!8ApMHs@vX`Z~;5hRr}Bz9k#kUc$D|(m}h@Y&|W+fjmkHiU#N84 zy$FaTPxVu~1@Oe$i>=jBk*O`UNs0s84`CGeG*)+Qh1)oQz~^$g;7W*5-wxDBU2D&c zXX?)L@Z2_h$!+O2Ajg~0mZ;f!3l@#R_uAUjY z*6wPra2PFZX%-MhWd{qqX07pcWZ>@IeWLkXNg(zr>f20-+k;(URFD&ji;HVr>*?zI zz;5T2YXa-kSxERh@vi^u>gpNuJ*3p`9;o*~E@#Kcmf6NUa{hr+4)>MsIS=nIHWdU^ojO=5eC<8W1=lgTREiHEe)T!%|y?|tG3re2} zR|NGsAOel0H?rzJEcyFzxd;4ZS}*`9514-e?$QCE3*`-e1jM89#4pxwv1&M%tl8K~d4+U(6Y)8GV`ktgOx@!%~91O>F+*BzCJEQ=-3iD}vLIzN^J)LXyXiCc3GCVGAq`C?) z52eDRD}qeyvzC_nWD;_??crUJs!bR|yR?TFSQR*^L8iji#x^f6N9rO~if=j?Tm3zq z13K>VXkK=8bxd+{vdNDdE2oXL%*@7DNMRpNN(kJquhp|hn^wDqQCmy~v$CaA3knWq z+!aix%Gz6dozG*>cNGfh=;)Fzw0Pki9T|4U(hcH}8{`5%0H+Ol75QSI>~W03*`Ml( ziG?W>5Nr7HZ#CzAky$6@W{tV~UG-1gk?#y zgPS?W;y_1eMCYo96ow#c#s{F{Olxp)xzHb5VvmlOI8OH_k&NLg8FpUDrjD`&7Fzt{+`2udFSir z&&P@_ARr6e0VL1Br{@(IY95RUY>qQP)_P}XVnn5C$$bSeFtRtH9v*nG3F1_Y=j5x` zdp*weZ=vfp&%ew;Ile++k&hodr2j8);eXU?U9_FfaklKQjjJ_i9B0nVX6=PtcQ*28 z8|IxJJq@IUuW)!g1~oT>aB`GYAHjzn5KuO_y0bms?|Z!3W4w`=03?`g_J=!b^=84G zV@2`u*DuhmdMj0_z?)g^kaWVr?kKmIZRo8oct0=lfu;s1iGl0LLO_`Sjqw79X|08y zv%vW-t((<;KZFw-tOlz@Zu`?^&+Cr>9!O2+`%ysh=MD!4M-*HJ(yv?0<-in~!Fc6# zT``RH%1*Ok1us!yAHbFYJ{7!^mjH$7;-pdMPe;-$# ztGG8`E-tBg7`5oOdGmqV1z>vIGbQ#3G~JFz*85c}snpLl{q;lvY^rxb7-)ildzh^- zx7}D9zHaPWKSit_jy@V61GsC-c-HY+#bAKD9sp8@b~TCgtE`o9=ZVkZaAzt_AkDns zEkCYn(4fLYQWE0k`T@t7AxpE;B#KgwBJJJn&bVL@rpo+4fj-i$WvZZ0ox`Wibs!0` z15}|6Rs}$I0~AMe4%g{#Zx#r1Lqo358%ux%Du6_m zOA3sGj*pN3bsJEqCE$9|t+5bW5)cwd3)W*i|Qfs~)G?@_LRtSs-3PE_`_^^J|k4staG zfC*P;z;D|M$MX1Cjr7)_zh-T1Ej<7Oj>GZ$w?X7-jly>D^4g-1q5+V^KvW&lu1 z&E8z1;LcM?KKm^embz8fyN%*P%>cZPwvO1in8y=KDS*%`F#$DmK*}mLX9*|(vy_>M zQk)G6O}<$%CDVAfmto}j`Yan&5 zUUQk@Y$*!T@~J7Ql-Doa-$;La{ehYALy*B|6mjwXfGA4Y%kf=C1O&ub2gk>CP)Y%e zw53tr;X?UZ31j0O;I6kpiAk%!c4Ud#Py;yvKshZ6K|#wz+01^S!7hj6mGxeXWS~=L zG?ulzCX5Pt1M0=jIB?A=D=Rl)o%9DvsZI}0BT<}pXQG7-upq9PQ&T5cq~_#==ow

@!*NQDnoeX^f+dh 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 Ks#N05`~L&uVOezm literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..47dc539 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module sing-vis + +go 1.24.7 + +require ( + github.com/sagernet/sing v0.8.12-0.20260721063414-596db5dd6ef4 + github.com/sagernet/sing-box v0.0.0-00010101000000-000000000000 + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba +) + +require ( + github.com/miekg/dns v1.1.72 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/tools v0.42.0 // indirect +) + +replace github.com/sagernet/sing-box => ./sing-box diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e703fa8 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sagernet/sing v0.8.12-0.20260721063414-596db5dd6ef4 h1:dIHd4IiQs0mtptfe8SdwDascDwh1w2LqDI8exoDuKeQ= +github.com/sagernet/sing v0.8.12-0.20260721063414-596db5dd6ef4/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/dnsx/doh.go b/internal/dnsx/doh.go new file mode 100644 index 0000000..51d184e --- /dev/null +++ b/internal/dnsx/doh.go @@ -0,0 +1,169 @@ +// Package dnsx provides a DNS-over-HTTPS resolver used to resolve domains to IP +// addresses when route rules depend on the resolved IP (ip_cidr, IP rule sets) +// or when a rule's action is "resolve". +// +// It uses the DoH JSON API (https://developers.google.com/speed/public-dns/docs/doh/json, +// also implemented by Cloudflare) rather than the RFC 8484 wireformat. In a +// browser this matters: a JSON GET with `Accept: application/dns-json` is a CORS +// "simple request", so it avoids the preflight that a wireformat POST with a +// custom Content-Type would trigger, and it needs no DNS message packer. +package dnsx + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" +) + +// Resolver resolves a hostname to A/AAAA records. +type Resolver interface { + // Resolve returns resolved addresses for name. strategy is one of + // "", "prefer_ipv4", "prefer_ipv6", "ipv4_only", "ipv6_only". + Resolve(ctx context.Context, name string, strategy string) (*Result, error) + Server() string +} + +// Result holds resolved addresses and diagnostic info about the query. +type Result struct { + Name string `json:"name"` + IPv4 []string `json:"ipv4"` + IPv6 []string `json:"ipv6"` + Error string `json:"error,omitempty"` +} + +// All returns v4+v6 addresses honoring the strategy ordering. +func (r *Result) All(strategy string) []string { + switch strategy { + case "ipv4_only": + return r.IPv4 + case "ipv6_only": + return r.IPv6 + case "prefer_ipv6": + return append(append([]string{}, r.IPv6...), r.IPv4...) + default: // prefer_ipv4 / unset + return append(append([]string{}, r.IPv4...), r.IPv6...) + } +} + +// DoHResolver implements Resolver against a DoH JSON endpoint. +type DoHResolver struct { + server string + client *http.Client + + mu sync.Mutex + cache map[string]*Result +} + +// NewDoHResolver builds a resolver for the given DoH endpoint URL. +func NewDoHResolver(server string) *DoHResolver { + return &DoHResolver{ + server: server, + client: &http.Client{Timeout: 10 * time.Second}, + cache: map[string]*Result{}, + } +} + +func (d *DoHResolver) Server() string { return d.server } + +// DNS record types used by the JSON API. +const ( + typeA = 1 + typeAAAA = 28 +) + +// Resolve queries A and AAAA records for name over DoH JSON, caching per resolver. +func (d *DoHResolver) Resolve(ctx context.Context, name string, strategy string) (*Result, error) { + name = strings.TrimSuffix(strings.ToLower(name), ".") + d.mu.Lock() + if r, ok := d.cache[name]; ok { + d.mu.Unlock() + return r, nil + } + d.mu.Unlock() + + res := &Result{Name: name} + var firstErr error + + if strategy != "ipv6_only" { + v4, err := d.query(ctx, name, typeA) + if err != nil { + firstErr = err + } + res.IPv4 = v4 + } + if strategy != "ipv4_only" { + v6, err := d.query(ctx, name, typeAAAA) + if err != nil && firstErr == nil { + firstErr = err + } + res.IPv6 = v6 + } + + if len(res.IPv4) == 0 && len(res.IPv6) == 0 && firstErr != nil { + res.Error = firstErr.Error() + return res, firstErr + } + d.mu.Lock() + d.cache[name] = res + d.mu.Unlock() + return res, nil +} + +// jsonResponse is the DoH JSON API response shape (Google/Cloudflare). +type jsonResponse struct { + Status int `json:"Status"` + Answer []struct { + Name string `json:"name"` + Type int `json:"type"` + Data string `json:"data"` + } `json:"Answer"` + Comment string `json:"Comment,omitempty"` +} + +func (d *DoHResolver) query(ctx context.Context, name string, qtype int) ([]string, error) { + endpoint, err := url.Parse(d.server) + if err != nil { + return nil, fmt.Errorf("invalid DoH server %q: %w", d.server, err) + } + q := endpoint.Query() + q.Set("name", name) + q.Set("type", fmt.Sprint(qtype)) + q.Set("ct", "application/dns-json") + endpoint.RawQuery = q.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, err + } + // Accept is a CORS-safelisted header, so this stays a simple request. + req.Header.Set("Accept", "application/dns-json") + resp, err := d.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("DoH status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + var parsed jsonResponse + if err := json.Unmarshal(body, &parsed); err != nil { + return nil, fmt.Errorf("parse DoH JSON: %w", err) + } + var out []string + for _, a := range parsed.Answer { + if a.Type == qtype { + out = append(out, a.Data) + } + } + return out, nil +} diff --git a/internal/engine/analyze.go b/internal/engine/analyze.go new file mode 100644 index 0000000..8ab0008 --- /dev/null +++ b/internal/engine/analyze.go @@ -0,0 +1,134 @@ +package engine + +import ( + "context" + "net/netip" + "strings" +) + +func analyze(ctx context.Context, req Request) (*Result, error) { + cfg, err := ParseConfig(req.Config) + if err != nil { + return nil, err + } + res := &Result{Warnings: append([]string{}, cfg.Warnings...)} + if req.Resolver != nil { + res.DoHServer = req.Resolver.Server() + } + rs := newRuleSetResolver(ctx, cfg, req.RuleSetFiles, &res.Warnings) + + for _, raw := range req.Inputs { + line := strings.TrimSpace(raw) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + res.Inputs = append(res.Inputs, analyzeInput(ctx, cfg, rs, req, line)) + } + return res, nil +} + +func analyzeInput(ctx context.Context, cfg *Config, rs *ruleSetResolver, req Request, line string) InputTrace { + it := InputTrace{Input: line} + host := normalizeInput(line) + ec := &evalCtx{ctx: ctx, network: req.Network, rs: rs, resolver: req.Resolver} + + if addr, ok := parseIPInput(host); ok { + it.Kind = "ip" + ec.destIsIP = true + ec.destAddr = addr + it.Route = ec.matchRoute(cfg) + return it + } + + if !looksLikeDomain(host) { + it.Kind = "invalid" + it.Error = "not a valid domain or IP address" + return it + } + + it.Kind = "domain" + host = strings.ToLower(strings.TrimSuffix(host, ".")) + ec.host = host + + // Resolve via DoH for display and (optionally) to let IP rules match. + if req.Resolver != nil { + r, _ := req.Resolver.Resolve(ctx, host, "") + if r != nil { + it.Resolved = &ResolvedInfo{Server: req.Resolver.Server(), IPv4: r.IPv4, IPv6: r.IPv6, Error: r.Error} + ec.resolved = it.Resolved + if req.AssumeResolved { + if addrs := parseAddrs(r.All("")); len(addrs) > 0 { + ec.setAddresses(addrs) + } + } + } else { + it.Resolved = &ResolvedInfo{Server: req.Resolver.Server(), Error: "resolution failed"} + } + } + + it.DNS = ec.matchDNS(cfg) + it.Route = ec.matchRoute(cfg) + return it +} + +// normalizeInput strips scheme, path, userinfo and a trailing :port so pasted +// URLs or host:port strings still analyze correctly. +func normalizeInput(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return s + } + if i := strings.Index(s, "://"); i >= 0 { + s = s[i+3:] + } + if i := strings.IndexByte(s, '/'); i >= 0 { + s = s[:i] + } + if i := strings.LastIndexByte(s, '@'); i >= 0 { + s = s[i+1:] + } + s = strings.TrimSpace(s) + // Strip a trailing :port for domains / IPv4 (but not raw IPv6 which has many colons). + if strings.Count(s, ":") == 1 { + host, port, ok := strings.Cut(s, ":") + if ok && isAllDigits(port) && host != "" { + s = host + } + } + // Strip [ ] around bracketed IPv6. + s = strings.TrimPrefix(s, "[") + s = strings.TrimSuffix(s, "]") + return s +} + +func parseIPInput(s string) (netip.Addr, bool) { + addr, err := netip.ParseAddr(strings.TrimSpace(s)) + if err != nil { + return netip.Addr{}, false + } + return addr, true +} + +func looksLikeDomain(s string) bool { + if s == "" || len(s) > 253 { + return false + } + for _, r := range s { + if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '.' || r == '-' || r == '_' || r == '*') { + return false + } + } + return true +} + +func isAllDigits(s string) bool { + if s == "" { + return false + } + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true +} diff --git a/internal/engine/conditions.go b/internal/engine/conditions.go new file mode 100644 index 0000000..ff8e667 --- /dev/null +++ b/internal/engine/conditions.go @@ -0,0 +1,473 @@ +package engine + +import ( + "context" + "fmt" + "net/netip" + "regexp" + "strings" + + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/domain" + "go4.org/netipx" + + "sing-vis/internal/dnsx" +) + +// Condition groups (see docs/configuration/route/rule.md matching formula). +const ( + groupDestAddr = "dest_addr" + groupSrcAddr = "src_addr" + groupDestPort = "dest_port" + groupSrcPort = "src_port" + groupRuleSet = "rule_set" + groupOther = "other" +) + +// evalCtx carries the per-input matching state. +// +// It intentionally does NOT use sing-box's adapter.InboundContext: importing the +// adapter package pulls in the full outbound/dialer/sing-tun dependency tree, +// which does not compile for GOARCH=wasm. The only rule fields that depend on an +// InboundContext are domain / network / query_type, and those are matched here +// directly (domain via sing/common/domain, the source of truth reused verbatim +// by route/rule.DomainItem; network / query_type are plain membership tests). +type evalCtx struct { + ctx context.Context + network string // assumed connection network ("", "tcp", "udp") + queryType uint16 // DNS query type in effect (0 = unknown) + host string // lowercased domain (empty for IP input) + destIsIP bool + destAddr netip.Addr + destResolved bool // addresses populated (via resolve/assume) + addresses []netip.Addr // resolved/assumed destination addresses + rs *ruleSetResolver + resolver dnsx.Resolver + resolved *ResolvedInfo // cached DoH result for the host +} + +// setAddresses records resolved destination addresses so IP-based conditions +// (ip_cidr, ip rule sets) can match them. +func (ec *evalCtx) setAddresses(addrs []netip.Addr) { + ec.addresses = addrs + ec.destResolved = true +} + +// matchFields is the normalized, matchable subset shared by route, DNS and +// headless rules. +type matchFields struct { + domain []string + domainSuffix []string + domainKeyword []string + domainRegex []string + ipCIDR []string + ipIsPrivate bool + srcIPCIDR []string + srcIPIsPriv bool + port []uint16 + portRange []string + srcPort []uint16 + srcPortRange []string + network []string + queryType []option.DNSQueryType + ruleSet []string + rsMatchSource bool + invert bool + + // Pre-compiled matchers from binary (.srs) rule sets. + rawDomain *domain.Matcher + rawIPSet *netipx.IPSet + + unknowns []condKV // fields we cannot evaluate offline (assumptions) + dnsFilter []condKV // DNS response-address filters (not applicable to query routing) +} + +type condKV struct{ field, value string } + +// orStatus combines OR-group members. +func orStatus(members []string) string { + unknown := false + for _, s := range members { + if s == StatusMatch { + return StatusMatch + } + if s == StatusUnknown { + unknown = true + } + } + if unknown { + return StatusUnknown + } + return StatusNoMatch +} + +// andStatus combines AND members. +func andStatus(members []string) string { + unknown := false + for _, s := range members { + if s == StatusNoMatch { + return StatusNoMatch + } + if s == StatusUnknown { + unknown = true + } + } + if unknown { + return StatusUnknown + } + return StatusMatch +} + +func invertStatus(s string) string { + switch s { + case StatusMatch: + return StatusNoMatch + case StatusNoMatch: + return StatusMatch + default: + return StatusUnknown + } +} + +// evalFields evaluates a normalized rule against the context, producing an +// overall tri-state status and the per-condition breakdown. An empty rule +// (no conditions) matches everything. +func (ec *evalCtx) evalFields(mf matchFields) (string, []CondEval) { + var conds []CondEval + var groupStatuses []string // AND across non-empty groups + other conds + + // --- destination address group (OR) --- + var da []string + if len(mf.domain) > 0 { + st, matched := ec.matchDomainExact(mf.domain) + conds = append(conds, CondEval{Field: "domain", Value: joinVals(mf.domain), Group: groupDestAddr, Status: st, Matched: matched}) + da = append(da, st) + } + if len(mf.domainSuffix) > 0 { + st, matched := ec.matchDomainSuffix(mf.domainSuffix) + conds = append(conds, CondEval{Field: "domain_suffix", Value: joinVals(mf.domainSuffix), Group: groupDestAddr, Status: st, Matched: matched}) + da = append(da, st) + } + if len(mf.domainKeyword) > 0 { + st, matched := ec.matchKeyword(mf.domainKeyword) + conds = append(conds, CondEval{Field: "domain_keyword", Value: joinVals(mf.domainKeyword), Group: groupDestAddr, Status: st, Matched: matched}) + da = append(da, st) + } + if len(mf.domainRegex) > 0 { + st, matched := ec.matchRegex(mf.domainRegex) + conds = append(conds, CondEval{Field: "domain_regex", Value: joinVals(mf.domainRegex), Group: groupDestAddr, Status: st, Matched: matched}) + da = append(da, st) + } + if mf.rawDomain != nil { + st := StatusNoMatch + if ec.host != "" && mf.rawDomain.Match(ec.host) { + st = StatusMatch + } + conds = append(conds, CondEval{Field: "domain/domain_suffix", Value: "«compiled set»", Group: groupDestAddr, Status: st}) + da = append(da, st) + } + if len(mf.ipCIDR) > 0 { + st, matched, note := ec.matchIPCIDR(mf.ipCIDR, false) + conds = append(conds, CondEval{Field: "ip_cidr", Value: joinVals(mf.ipCIDR), Group: groupDestAddr, Status: st, Matched: matched, Note: note}) + da = append(da, st) + } + if mf.rawIPSet != nil { + st, note := ec.matchRawIPSet(mf.rawIPSet) + conds = append(conds, CondEval{Field: "ip_cidr", Value: "«compiled set»", Group: groupDestAddr, Status: st, Note: note}) + da = append(da, st) + } + if mf.ipIsPrivate { + st, note := ec.matchIPIsPrivate(false) + conds = append(conds, CondEval{Field: "ip_is_private", Value: "true", Group: groupDestAddr, Status: st, Note: note}) + da = append(da, st) + } + if len(da) > 0 { + groupStatuses = append(groupStatuses, orStatus(da)) + } + + // --- source address group (OR) — source is unknown offline --- + var sa []string + if len(mf.srcIPCIDR) > 0 { + conds = append(conds, CondEval{Field: "source_ip_cidr", Value: joinVals(mf.srcIPCIDR), Group: groupSrcAddr, Status: StatusUnknown, Note: "client source address is unknown"}) + sa = append(sa, StatusUnknown) + } + if mf.srcIPIsPriv { + conds = append(conds, CondEval{Field: "source_ip_is_private", Value: "true", Group: groupSrcAddr, Status: StatusUnknown, Note: "client source address is unknown"}) + sa = append(sa, StatusUnknown) + } + if len(sa) > 0 { + groupStatuses = append(groupStatuses, orStatus(sa)) + } + + // --- destination port group (OR) — port is unknown for a bare domain/IP --- + var dp []string + if len(mf.port) > 0 { + conds = append(conds, CondEval{Field: "port", Value: joinU16(mf.port), Group: groupDestPort, Status: StatusUnknown, Note: "destination port is not part of the query"}) + dp = append(dp, StatusUnknown) + } + if len(mf.portRange) > 0 { + conds = append(conds, CondEval{Field: "port_range", Value: joinVals(mf.portRange), Group: groupDestPort, Status: StatusUnknown, Note: "destination port is not part of the query"}) + dp = append(dp, StatusUnknown) + } + if len(dp) > 0 { + groupStatuses = append(groupStatuses, orStatus(dp)) + } + + // --- source port group (OR) — unknown --- + var sp []string + if len(mf.srcPort) > 0 { + conds = append(conds, CondEval{Field: "source_port", Value: joinU16(mf.srcPort), Group: groupSrcPort, Status: StatusUnknown, Note: "client source port is unknown"}) + sp = append(sp, StatusUnknown) + } + if len(mf.srcPortRange) > 0 { + conds = append(conds, CondEval{Field: "source_port_range", Value: joinVals(mf.srcPortRange), Group: groupSrcPort, Status: StatusUnknown, Note: "client source port is unknown"}) + sp = append(sp, StatusUnknown) + } + if len(sp) > 0 { + groupStatuses = append(groupStatuses, orStatus(sp)) + } + + // --- rule_set group (OR across tags) --- + if len(mf.ruleSet) > 0 { + var rsStatuses []string + for _, tag := range mf.ruleSet { + rse := ec.rs.evaluate(tag, ec, mf.rsMatchSource) + conds = append(conds, CondEval{Field: "rule_set", Value: tag, Group: groupRuleSet, Status: rse.Status, RuleSet: rse}) + rsStatuses = append(rsStatuses, rse.Status) + } + groupStatuses = append(groupStatuses, orStatus(rsStatuses)) + } + + // --- "other" fields (AND) --- + if len(mf.network) > 0 { + st := ec.matchNetwork(mf.network) + note := "" + if st == StatusUnknown { + note = "connection network (tcp/udp) not specified" + } + conds = append(conds, CondEval{Field: "network", Value: joinVals(mf.network), Group: groupOther, Status: st, Note: note}) + groupStatuses = append(groupStatuses, st) + } + if len(mf.queryType) > 0 { + st, matched := ec.matchQueryType(mf.queryType) + conds = append(conds, CondEval{Field: "query_type", Value: queryTypeList(mf.queryType), Group: groupOther, Status: st, Matched: matched, Note: "evaluated for the DNS query type shown"}) + groupStatuses = append(groupStatuses, st) + } + // DNS response-address filters: not applicable to query-routing. + for _, kv := range mf.dnsFilter { + conds = append(conds, CondEval{Field: kv.field, Value: kv.value, Group: groupOther, Status: StatusUnknown, Note: "matches the DNS response addresses, evaluated after resolution"}) + groupStatuses = append(groupStatuses, StatusUnknown) + } + // Unknown/undeterminable fields (protocol, process, clash_mode, ...). + for _, kv := range mf.unknowns { + conds = append(conds, CondEval{Field: kv.field, Value: kv.value, Group: groupOther, Status: StatusUnknown, Note: "cannot be determined offline"}) + groupStatuses = append(groupStatuses, StatusUnknown) + } + + status := StatusMatch + if len(groupStatuses) > 0 { + status = andStatus(groupStatuses) + } + if mf.invert { + status = invertStatus(status) + } + return status, conds +} + +// ---- individual matchers (reusing sing-box primitives where useful) ---- + +func (ec *evalCtx) matchDomainExact(domains []string) (string, string) { + if ec.host == "" { + return StatusNoMatch, "" + } + if domainMatcher(domains, nil).Match(ec.host) { + for _, d := range domains { + if strings.EqualFold(strings.TrimSuffix(d, "."), ec.host) { + return StatusMatch, d + } + } + return StatusMatch, "" + } + return StatusNoMatch, "" +} + +func (ec *evalCtx) matchDomainSuffix(suffixes []string) (string, string) { + if ec.host == "" { + return StatusNoMatch, "" + } + if domainMatcher(nil, suffixes).Match(ec.host) { + for _, s := range suffixes { + if domainMatcher(nil, []string{s}).Match(ec.host) { + return StatusMatch, s + } + } + return StatusMatch, "" + } + return StatusNoMatch, "" +} + +// domainMatcher builds a sing/common/domain matcher for the given exact domains +// and suffixes. This mirrors route/rule.NewDomainItem exactly (it calls +// domain.NewMatcher(domains, domainSuffixes, false)), so matching stays faithful +// to sing-box's succinct-set suffix logic without importing route/rule. +func domainMatcher(domains, suffixes []string) *domain.Matcher { + return domain.NewMatcher(domains, suffixes, false) +} + +func (ec *evalCtx) matchKeyword(keywords []string) (string, string) { + if ec.host == "" { + return StatusNoMatch, "" + } + for _, kw := range keywords { + if kw != "" && strings.Contains(ec.host, strings.ToLower(kw)) { + return StatusMatch, kw + } + } + return StatusNoMatch, "" +} + +func (ec *evalCtx) matchRegex(exprs []string) (string, string) { + if ec.host == "" { + return StatusNoMatch, "" + } + for _, e := range exprs { + re, err := regexp.Compile(e) + if err != nil { + continue + } + if re.MatchString(ec.host) { + return StatusMatch, e + } + } + return StatusNoMatch, "" +} + +// matchIPCIDR evaluates an ip_cidr condition. For a domain destination it is +// UNKNOWN until addresses are resolved; then it matches those addresses. +func (ec *evalCtx) matchIPCIDR(cidrs []string, isSource bool) (string, string, string) { + if isSource { + return StatusUnknown, "", "client source address is unknown" + } + addrs := ec.matchAddrs() + if len(addrs) == 0 { + if ec.host != "" { + return StatusUnknown, "", "requires the resolved IP (domain not resolved for this evaluation)" + } + return StatusNoMatch, "", "" + } + for _, cidr := range cidrs { + p, err := netip.ParsePrefix(strings.TrimSpace(cidr)) + if err != nil { + continue + } + for _, a := range addrs { + if p.Contains(a.Unmap()) || p.Contains(a) { + return StatusMatch, cidr, "" + } + } + } + return StatusNoMatch, "", "" +} + +func (ec *evalCtx) matchRawIPSet(set *netipx.IPSet) (string, string) { + addrs := ec.matchAddrs() + if len(addrs) == 0 { + if ec.host != "" { + return StatusUnknown, "requires the resolved IP" + } + return StatusNoMatch, "" + } + for _, a := range addrs { + if set.Contains(a.Unmap()) || set.Contains(a) { + return StatusMatch, "" + } + } + return StatusNoMatch, "" +} + +func (ec *evalCtx) matchIPIsPrivate(isSource bool) (string, string) { + if isSource { + return StatusUnknown, "client source address is unknown" + } + addrs := ec.matchAddrs() + if len(addrs) == 0 { + if ec.host != "" { + return StatusUnknown, "requires the resolved IP" + } + return StatusNoMatch, "" + } + for _, a := range addrs { + if a.IsPrivate() || a.IsLoopback() || a.IsLinkLocalUnicast() { + return StatusMatch, "" + } + } + return StatusNoMatch, "" +} + +// matchAddrs returns the destination addresses available for IP matching. +func (ec *evalCtx) matchAddrs() []netip.Addr { + if ec.destIsIP { + return []netip.Addr{ec.destAddr} + } + return ec.addresses +} + +func (ec *evalCtx) matchNetwork(networks []string) string { + if ec.network == "" { + return StatusUnknown + } + // route/rule.NetworkItem matches when the connection network is in the set. + for _, n := range networks { + if n == ec.network { + return StatusMatch + } + } + return StatusNoMatch +} + +func (ec *evalCtx) matchQueryType(types []option.DNSQueryType) (string, string) { + if ec.queryType == 0 { + return StatusUnknown, "" + } + // route/rule.QueryTypeItem matches when the query type is in the set. + for _, t := range types { + if uint16(t) == ec.queryType { + return StatusMatch, queryTypeName(ec.queryType) + } + } + return StatusNoMatch, "" +} + +// ---- display helpers ---- + +func joinVals(v []string) string { + if len(v) <= 4 { + return strings.Join(v, ", ") + } + return strings.Join(v[:4], ", ") + fmt.Sprintf(", …(+%d)", len(v)-4) +} + +func joinU16(v []uint16) string { + parts := make([]string, 0, len(v)) + for _, p := range v { + parts = append(parts, fmt.Sprint(p)) + } + return joinVals(parts) +} + +func queryTypeList(types []option.DNSQueryType) string { + parts := make([]string, 0, len(types)) + for _, t := range types { + parts = append(parts, queryTypeName(uint16(t))) + } + return strings.Join(parts, ", ") +} + +var queryTypeNames = map[uint16]string{1: "A", 28: "AAAA", 5: "CNAME", 15: "MX", 16: "TXT", 12: "PTR", 33: "SRV", 65: "HTTPS", 64: "SVCB"} + +func queryTypeName(t uint16) string { + if n, ok := queryTypeNames[t]; ok { + return n + } + return fmt.Sprintf("TYPE%d", t) +} diff --git a/internal/engine/dns.go b/internal/engine/dns.go new file mode 100644 index 0000000..acedee3 --- /dev/null +++ b/internal/engine/dns.go @@ -0,0 +1,111 @@ +package engine + +import ( + "github.com/sagernet/sing-box/option" +) + +func dnsActionOf(r option.DNSRule) actionInfo { + var a option.DNSRuleAction + if r.Type == "logical" { + a = r.LogicalOptions.DNSRuleAction + } else { + a = r.DefaultOptions.DNSRuleAction + } + typ := a.Action + if typ == "" { + typ = "route" + } + ai := actionInfo{typ: typ} + switch typ { + case "route": + ai.server = a.RouteOptions.Server + ai.terminal = true + ai.detail = "route → server " + orDefault(ai.server, "(default)") + case "route-options": + ai.detail = "route-options (non-terminal)" + case "reject": + m := a.RejectOptions.Method + if m == "" { + m = "default" + } + ai.terminal = true + ai.detail = "reject (" + m + ")" + case "predefined": + ai.terminal = true + ai.detail = "predefined response" + case "evaluate": + ai.server = a.RouteOptions.Server + ai.detail = "evaluate (non-terminal)" + case "respond": + ai.terminal = true + ai.detail = "respond" + default: + ai.terminal = true + ai.detail = typ + } + return ai +} + +// matchDNS evaluates DNS rules for the host to determine which DNS server / DNS +// rule action is hit. Evaluated for A queries (the common resolution path). +func (ec *evalCtx) matchDNS(cfg *Config) *DNSTrace { + prevQT := ec.queryType + ec.queryType = 1 // dns.TypeA + defer func() { ec.queryType = prevQT }() + + tr := &DNSTrace{QueryType: "A", MatchedIndex: -1, Final: cfg.effectiveDNSFinal()} + hadConditional := false + + for i, r := range cfg.DNSRules { + re := ec.evalDNSRuleNode(r) + re.Index = i + re.Reached = true + a := dnsActionOf(r) + re.ActionType = a.typ + re.ActionText = a.detail + re.Terminal = a.terminal + + switch re.Status { + case StatusMatch: + if !a.terminal { + re.Effect = "matched but non-terminal; continues scanning" + tr.Steps = append(tr.Steps, re) + continue + } + tr.Steps = append(tr.Steps, re) + tr.MatchedIndex = i + tr.Decision = ec.dnsDecision(cfg, a, false, hadConditional) + return tr + case StatusUnknown: + if a.terminal { + re.Effect = "could match here if its undetermined conditions hold" + hadConditional = true + } + tr.Steps = append(tr.Steps, re) + default: + tr.Steps = append(tr.Steps, re) + } + } + + // Fall through to dns.final. + final := cfg.effectiveDNSFinal() + tr.Decision = ec.dnsDecision(cfg, actionInfo{typ: "route", server: final, terminal: true, detail: "route → server " + orDefault(final, "(first server)")}, true, hadConditional) + if len(cfg.DNSRules) == 0 { + tr.Note = "no DNS rules; the final server is always used" + } + return tr +} + +func (ec *evalCtx) dnsDecision(cfg *Config, a actionInfo, fromFinal, assumed bool) *DNSDecision { + d := &DNSDecision{ + ActionType: a.typ, + Server: a.server, + Detail: a.detail, + FromFinal: fromFinal, + Assumed: assumed, + } + if a.server != "" { + d.ServerInfo = cfg.findDNSServer(a.server) + } + return d +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 0000000..1e9e1fd --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,42 @@ +// Package engine evaluates a sing-box configuration against a domain or IP and +// produces a step-by-step explanation of how DNS and route rules match. +package engine + +import ( + "context" + + "sing-vis/internal/dnsx" +) + +// RuleSetFile is an uploaded rule-set payload for a type:local rule set, keyed by +// the rule-set tag (or its configured path). Format is "source" or "binary"; for +// "binary" Data is base64-encoded, for "source" it is the raw JSON text. +type RuleSetFile struct { + Format string `json:"format"` + Data string `json:"data"` +} + +// Request bundles everything needed to analyze a set of inputs. +type Request struct { + Config string + Inputs []string + RuleSetFiles map[string]RuleSetFile + Network string // optional assumed network: "", "tcp", "udp" + // AssumeResolved pre-resolves domains via DoH before route matching so that + // ip_cidr / IP rule-set rules can match the resolved addresses (matching user + // intuition). When false, IP rules only match after an explicit resolve action. + AssumeResolved bool + Resolver dnsx.Resolver +} + +// Result is the top-level analysis response. +type Result struct { + DoHServer string `json:"dohServer"` + Warnings []string `json:"warnings,omitempty"` + Inputs []InputTrace `json:"inputs"` +} + +// Analyze is the entry point; implemented in analyze.go. +func Analyze(ctx context.Context, req Request) (*Result, error) { + return analyze(ctx, req) +} diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go new file mode 100644 index 0000000..83b007c --- /dev/null +++ b/internal/engine/engine_test.go @@ -0,0 +1,208 @@ +package engine + +import ( + "context" + "testing" + + "sing-vis/internal/dnsx" +) + +// fakeResolver returns canned DoH answers so route/DNS matching is testable +// offline (no network). Unknown names resolve to no addresses. +type fakeResolver struct { + answers map[string]*dnsx.Result +} + +func (f *fakeResolver) Server() string { return "fake-doh" } + +func (f *fakeResolver) Resolve(_ context.Context, name string, _ string) (*dnsx.Result, error) { + if r, ok := f.answers[name]; ok { + return r, nil + } + return &dnsx.Result{Name: name}, nil +} + +func analyzeOne(t *testing.T, cfg, input string, assumeResolved bool, res dnsx.Resolver) InputTrace { + t.Helper() + out, err := Analyze(context.Background(), Request{ + Config: cfg, + Inputs: []string{input}, + AssumeResolved: assumeResolved, + Resolver: res, + }) + if err != nil { + t.Fatalf("Analyze(%q) error: %v", input, err) + } + if len(out.Inputs) != 1 { + t.Fatalf("Analyze(%q): expected 1 input trace, got %d", input, len(out.Inputs)) + } + return out.Inputs[0] +} + +func routeOutbound(t *testing.T, it InputTrace) *RouteDecision { + t.Helper() + if it.Route == nil || it.Route.Decision == nil { + t.Fatalf("input %q: missing route decision", it.Input) + } + return it.Route.Decision +} + +func TestDomainSuffix(t *testing.T) { + cfg := `{"route":{"rules":[{"domain_suffix":["google.com"],"outbound":"proxy"}],"final":"direct"}}` + + it := analyzeOne(t, cfg, "www.google.com", true, &fakeResolver{}) + d := routeOutbound(t, it) + if d.Outbound != "proxy" || d.FromFinal { + t.Errorf("www.google.com: got outbound=%q fromFinal=%v, want proxy/false", d.Outbound, d.FromFinal) + } + if it.Route.SelectedIndex != 0 { + t.Errorf("www.google.com: selectedIndex=%d, want 0", it.Route.SelectedIndex) + } + + it = analyzeOne(t, cfg, "example.org", true, &fakeResolver{}) + d = routeOutbound(t, it) + if d.Outbound != "direct" || !d.FromFinal { + t.Errorf("example.org: got outbound=%q fromFinal=%v, want direct/true", d.Outbound, d.FromFinal) + } +} + +func TestInlineRuleSet(t *testing.T) { + cfg := `{"route":{ + "rules":[{"rule_set":["cn"],"outbound":"direct"}], + "rule_set":[{"type":"inline","tag":"cn","rules":[{"domain_suffix":["baidu.com"]}]}], + "final":"proxy"}}` + + it := analyzeOne(t, cfg, "www.baidu.com", true, &fakeResolver{}) + d := routeOutbound(t, it) + if d.Outbound != "direct" { + t.Errorf("www.baidu.com: outbound=%q, want direct", d.Outbound) + } + // The rule_set condition should report a match with a non-negative matched idx. + step := it.Route.Steps[0] + if step.Status != StatusMatch { + t.Errorf("rule_set step status=%q, want match", step.Status) + } + var found bool + for _, c := range step.Conditions { + if c.Field == "rule_set" && c.RuleSet != nil { + found = true + if c.RuleSet.Status != StatusMatch || c.RuleSet.MatchedIdx != 0 { + t.Errorf("rule_set eval: status=%q matchedIdx=%d, want match/0", c.RuleSet.Status, c.RuleSet.MatchedIdx) + } + } + } + if !found { + t.Error("no rule_set condition found in step") + } + + it = analyzeOne(t, cfg, "www.google.com", true, &fakeResolver{}) + if d := routeOutbound(t, it); d.Outbound != "proxy" || !d.FromFinal { + t.Errorf("www.google.com: outbound=%q fromFinal=%v, want proxy/true", d.Outbound, d.FromFinal) + } +} + +func TestLogicalInvert(t *testing.T) { + // Inverted logical rule: matches everything EXCEPT *.google.com. + cfg := `{"route":{"rules":[ + {"type":"logical","mode":"and","invert":true, + "rules":[{"domain_suffix":["google.com"]}],"outbound":"not-google"} + ],"final":"proxy"}}` + + it := analyzeOne(t, cfg, "example.com", true, &fakeResolver{}) + if d := routeOutbound(t, it); d.Outbound != "not-google" { + t.Errorf("example.com: outbound=%q, want not-google (inverted match)", d.Outbound) + } + + it = analyzeOne(t, cfg, "www.google.com", true, &fakeResolver{}) + if d := routeOutbound(t, it); d.Outbound != "proxy" || !d.FromFinal { + t.Errorf("www.google.com: outbound=%q fromFinal=%v, want proxy/true (inverted no-match)", d.Outbound, d.FromFinal) + } +} + +func TestResolveThenIPCIDR(t *testing.T) { + cfg := `{"route":{"rules":[ + {"domain_suffix":["example.com"],"action":"resolve"}, + {"ip_cidr":["1.2.3.0/24"],"outbound":"matched-ip"} + ],"final":"proxy"}}` + res := &fakeResolver{answers: map[string]*dnsx.Result{ + "host.example.com": {Name: "host.example.com", IPv4: []string{"1.2.3.4"}}, + }} + + // AssumeResolved=false: ip_cidr only matches after the explicit resolve action. + it := analyzeOne(t, cfg, "host.example.com", false, res) + if d := routeOutbound(t, it); d.Outbound != "matched-ip" { + t.Errorf("host.example.com: outbound=%q, want matched-ip", d.Outbound) + } + // The resolve step should carry an effect line mentioning the resolved IP. + if it.Route.Steps[0].Effect == "" { + t.Error("resolve step missing effect line") + } + + // A domain that resolves outside the CIDR falls through to final. + res2 := &fakeResolver{answers: map[string]*dnsx.Result{ + "other.example.com": {Name: "other.example.com", IPv4: []string{"9.9.9.9"}}, + }} + it = analyzeOne(t, cfg, "other.example.com", false, res2) + if d := routeOutbound(t, it); d.Outbound != "proxy" || !d.FromFinal { + t.Errorf("other.example.com: outbound=%q fromFinal=%v, want proxy/true", d.Outbound, d.FromFinal) + } +} + +func TestDNSServerSelection(t *testing.T) { + cfg := `{"dns":{ + "servers":[ + {"tag":"proxy-dns","type":"https","server":"1.1.1.1","detour":"proxy"}, + {"tag":"local-dns","type":"udp","server":"223.5.5.5","detour":"direct"} + ], + "rules":[{"domain_suffix":["cn.example"],"server":"local-dns"}], + "final":"proxy-dns"}}` + + it := analyzeOne(t, cfg, "site.cn.example", true, &fakeResolver{}) + if it.DNS == nil || it.DNS.Decision == nil { + t.Fatal("missing DNS decision") + } + d := it.DNS.Decision + if d.Server != "local-dns" || d.FromFinal { + t.Errorf("site.cn.example: dns server=%q fromFinal=%v, want local-dns/false", d.Server, d.FromFinal) + } + if d.ServerInfo == nil || d.ServerInfo.Detour != "direct" { + t.Errorf("site.cn.example: dns detour=%v, want direct", d.ServerInfo) + } + + it = analyzeOne(t, cfg, "other.example", true, &fakeResolver{}) + d = it.DNS.Decision + if d.Server != "proxy-dns" || !d.FromFinal { + t.Errorf("other.example: dns server=%q fromFinal=%v, want proxy-dns/true", d.Server, d.FromFinal) + } + if d.ServerInfo == nil || d.ServerInfo.Detour != "proxy" { + t.Errorf("other.example: dns detour=%v, want proxy", d.ServerInfo) + } +} + +func TestRawIPInput(t *testing.T) { + cfg := `{"route":{"rules":[{"ip_cidr":["10.0.0.0/8"],"outbound":"lan"}],"final":"wan"}}` + + it := analyzeOne(t, cfg, "10.1.2.3", true, &fakeResolver{}) + if it.Kind != "ip" { + t.Errorf("10.1.2.3: kind=%q, want ip", it.Kind) + } + if it.DNS != nil { + t.Error("raw IP should have no DNS trace") + } + if d := routeOutbound(t, it); d.Outbound != "lan" { + t.Errorf("10.1.2.3: outbound=%q, want lan", d.Outbound) + } + + it = analyzeOne(t, cfg, "8.8.8.8", true, &fakeResolver{}) + if d := routeOutbound(t, it); d.Outbound != "wan" || !d.FromFinal { + t.Errorf("8.8.8.8: outbound=%q fromFinal=%v, want wan/true", d.Outbound, d.FromFinal) + } +} + +func TestInvalidInput(t *testing.T) { + cfg := `{"route":{"rules":[],"final":"proxy"}}` + it := analyzeOne(t, cfg, "not a valid host!!", true, &fakeResolver{}) + if it.Kind != "invalid" { + t.Errorf("kind=%q, want invalid", it.Kind) + } +} diff --git a/internal/engine/parse.go b/internal/engine/parse.go new file mode 100644 index 0000000..9599154 --- /dev/null +++ b/internal/engine/parse.go @@ -0,0 +1,128 @@ +package engine + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/sagernet/sing-box/option" + sjson "github.com/sagernet/sing/common/json" +) + +// Config is the parsed subset of a sing-box configuration relevant to routing. +type Config struct { + RouteRules []option.Rule + RouteRuleSets []option.RuleSet + RouteFinal string + + DNSRules []option.DNSRule + DNSFinal string + DNSServers []DNSServerInfo + + Warnings []string +} + +// ParseConfig parses a raw sing-box JSON (JSONC allowed) configuration into the +// routing-relevant structures, using sing-box's own option unmarshalers so that +// rule/action/rule-set dispatch is version-accurate. +func ParseConfig(text string) (*Config, error) { + text = strings.TrimSpace(text) + if text == "" { + return nil, fmt.Errorf("empty configuration") + } + ctx := context.Background() + var raw map[string]json.RawMessage + if err := sjson.UnmarshalContext(ctx, []byte(text), &raw); err != nil { + return nil, fmt.Errorf("invalid JSON: %w", err) + } + + cfg := &Config{} + + if rm, ok := raw["route"]; ok && len(rm) > 0 { + var route struct { + Rules []option.Rule `json:"rules"` + RuleSet []option.RuleSet `json:"rule_set"` + Final string `json:"final"` + } + if err := sjson.UnmarshalContext(ctx, rm, &route); err != nil { + return nil, fmt.Errorf("route: %w", err) + } + cfg.RouteRules = route.Rules + cfg.RouteRuleSets = route.RuleSet + cfg.RouteFinal = route.Final + } + + if rm, ok := raw["dns"]; ok && len(rm) > 0 { + var dnsSec struct { + Rules []option.DNSRule `json:"rules"` + Final string `json:"final"` + Servers []json.RawMessage `json:"servers"` + } + if err := sjson.UnmarshalContext(ctx, rm, &dnsSec); err != nil { + return nil, fmt.Errorf("dns: %w", err) + } + cfg.DNSRules = dnsSec.Rules + cfg.DNSFinal = dnsSec.Final + cfg.DNSServers = parseDNSServers(dnsSec.Servers) + } + + return cfg, nil +} + +// parseDNSServers extracts display metadata from dns.servers generically so we +// don't need the DNS transport registry (which would pull in the whole protocol +// dependency tree). +func parseDNSServers(servers []json.RawMessage) []DNSServerInfo { + var out []DNSServerInfo + for _, raw := range servers { + var m map[string]any + if json.Unmarshal(raw, &m) != nil { + continue + } + info := DNSServerInfo{ + Tag: asString(m["tag"]), + Type: asString(m["type"]), + Detour: asString(m["detour"]), + Address: firstString(m, "server", "address"), + } + out = append(out, info) + } + return out +} + +func asString(v any) string { + s, _ := v.(string) + return s +} + +func firstString(m map[string]any, keys ...string) string { + for _, k := range keys { + if s, ok := m[k].(string); ok && s != "" { + return s + } + } + return "" +} + +// findDNSServer returns the server info for a tag, if present. +func (c *Config) findDNSServer(tag string) *DNSServerInfo { + for i := range c.DNSServers { + if c.DNSServers[i].Tag == tag { + return &c.DNSServers[i] + } + } + return nil +} + +// effectiveDNSFinal returns the DNS server tag used when no rule matches: the +// configured dns.final, or the first server tag if unset. +func (c *Config) effectiveDNSFinal() string { + if c.DNSFinal != "" { + return c.DNSFinal + } + if len(c.DNSServers) > 0 { + return c.DNSServers[0].Tag + } + return "" +} diff --git a/internal/engine/route.go b/internal/engine/route.go new file mode 100644 index 0000000..2062904 --- /dev/null +++ b/internal/engine/route.go @@ -0,0 +1,185 @@ +package engine + +import ( + "net/netip" + "strings" + + "github.com/sagernet/sing-box/option" +) + +type actionInfo struct { + typ string + outbound string + detail string + terminal bool + isResolve bool + strategy string + server string +} + +func routeActionOf(r option.Rule) actionInfo { + var a option.RuleAction + if r.Type == "logical" { + a = r.LogicalOptions.RuleAction + } else { + a = r.DefaultOptions.RuleAction + } + typ := a.Action + if typ == "" { + typ = "route" + } + ai := actionInfo{typ: typ} + switch typ { + case "route": + ai.outbound = a.RouteOptions.Outbound + ai.terminal = true + ai.detail = "route → " + orDefault(ai.outbound, "(default outbound)") + case "route-options": + ai.detail = "route-options (non-terminal)" + case "reject": + m := a.RejectOptions.Method + if m == "" { + m = "default" + } + ai.terminal = true + ai.detail = "reject (" + m + ")" + case "hijack-dns": + ai.terminal = true + ai.detail = "hijack-dns" + case "sniff": + ai.detail = "sniff (non-terminal)" + case "resolve": + ai.isResolve = true + ai.strategy = safeStrategy(a.ResolveOptions.Strategy) + ai.server = a.ResolveOptions.Server + ai.detail = "resolve" + if ai.strategy != "" { + ai.detail += " (" + ai.strategy + ")" + } + case "direct": + ai.terminal = true + ai.detail = "direct" + ai.outbound = "direct" + case "bypass": + ai.outbound = a.BypassOptions.Outbound + ai.terminal = ai.outbound != "" + ai.detail = "bypass" + default: + ai.terminal = true + ai.detail = typ + } + return ai +} + +// matchRoute evaluates route rules top-to-bottom, first terminal match wins, +// handling non-terminal resolve/sniff actions and the final fallback. +func (ec *evalCtx) matchRoute(cfg *Config) *RouteTrace { + tr := &RouteTrace{SelectedIndex: -1, Final: cfg.RouteFinal} + hadConditional := false + + for i, r := range cfg.RouteRules { + re := ec.evalRuleNode(r, false) + re.Index = i + re.Reached = true + a := routeActionOf(r) + re.ActionType = a.typ + re.ActionText = a.detail + re.Terminal = a.terminal + + switch re.Status { + case StatusMatch: + if a.isResolve { + addrs := ec.performResolve(a.strategy) + if len(addrs) > 0 { + re.Effect = "resolved → " + strings.Join(addrStrings(addrs), ", ") + " (IP rules below can now match)" + } else { + re.Effect = "resolve produced no addresses" + } + tr.Steps = append(tr.Steps, re) + continue + } + if !a.terminal { + re.Effect = "matched but non-terminal; continues scanning" + tr.Steps = append(tr.Steps, re) + continue + } + tr.Steps = append(tr.Steps, re) + tr.SelectedIndex = i + tr.Decision = &RouteDecision{ + ActionType: a.typ, + Outbound: a.outbound, + Detail: a.detail, + Assumed: hadConditional, + } + return tr + case StatusUnknown: + if a.terminal { + re.Effect = "could match here if its undetermined conditions hold" + hadConditional = true + } + tr.Steps = append(tr.Steps, re) + default: + tr.Steps = append(tr.Steps, re) + } + } + + tr.Decision = &RouteDecision{ + ActionType: "route", + Outbound: effectiveRouteFinal(cfg), + Detail: "route → " + orDefault(effectiveRouteFinal(cfg), "(first outbound)"), + FromFinal: true, + Assumed: hadConditional, + } + return tr +} + +// performResolve resolves the host via DoH and records the addresses so IP-based +// rules below can match. It reuses any cached resolution. +func (ec *evalCtx) performResolve(strategy string) []netip.Addr { + if ec.host == "" || ec.resolver == nil { + return ec.addresses + } + res, _ := ec.resolver.Resolve(ec.ctx, ec.host, strategy) + if res == nil { + return ec.addresses + } + addrs := parseAddrs(res.All(strategy)) + if len(addrs) > 0 { + ec.setAddresses(addrs) + } + return addrs +} + +func effectiveRouteFinal(cfg *Config) string { + return cfg.RouteFinal // empty => sing-box uses the first outbound +} + +func orDefault(s, def string) string { + if s == "" { + return def + } + return s +} + +func safeStrategy(s option.DomainStrategy) (out string) { + defer func() { _ = recover() }() + return s.String() +} + +func parseAddrs(ss []string) []netip.Addr { + var out []netip.Addr + for _, s := range ss { + if a, err := netip.ParseAddr(s); err == nil { + out = append(out, a) + } + } + return out +} + +func addrStrings(addrs []netip.Addr) []string { + out := make([]string, 0, len(addrs)) + for _, a := range addrs { + out = append(out, a.String()) + } + return out +} diff --git a/internal/engine/rules.go b/internal/engine/rules.go new file mode 100644 index 0000000..ac00db5 --- /dev/null +++ b/internal/engine/rules.go @@ -0,0 +1,314 @@ +package engine + +import ( + "strings" + + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/json/badoption" +) + +// ---- field extraction ---- + +func fieldsFromRoute(r option.RawDefaultRule) matchFields { + mf := matchFields{ + domain: r.Domain, + domainSuffix: r.DomainSuffix, + domainKeyword: r.DomainKeyword, + domainRegex: r.DomainRegex, + ipCIDR: r.IPCIDR, + ipIsPrivate: r.IPIsPrivate, + srcIPCIDR: r.SourceIPCIDR, + srcIPIsPriv: r.SourceIPIsPrivate, + port: r.Port, + portRange: r.PortRange, + srcPort: r.SourcePort, + srcPortRange: r.SourcePortRange, + network: r.Network, + ruleSet: r.RuleSet, + rsMatchSource: r.RuleSetIPCIDRMatchSource || r.Deprecated_RulesetIPCIDRMatchSource, + invert: r.Invert, + } + addUnknownList(&mf, "inbound", r.Inbound) + addUnknownList(&mf, "protocol", r.Protocol) + addUnknownList(&mf, "client", r.Client) + addUnknownList(&mf, "auth_user", r.AuthUser) + addUnknownList(&mf, "user", r.User) + addUnknownList(&mf, "process_name", r.ProcessName) + addUnknownList(&mf, "process_path", r.ProcessPath) + addUnknownList(&mf, "process_path_regex", r.ProcessPathRegex) + addUnknownList(&mf, "package_name", r.PackageName) + addUnknownList(&mf, "package_name_regex", r.PackageNameRegex) + addUnknownList(&mf, "wifi_ssid", r.WIFISSID) + addUnknownList(&mf, "wifi_bssid", r.WIFIBSSID) + addUnknownList(&mf, "source_mac_address", r.SourceMACAddress) + addUnknownList(&mf, "source_hostname", r.SourceHostname) + addUnknownList(&mf, "preferred_by", r.PreferredBy) + addUnknownDeprecated(&mf, "geosite", r.Geosite) + addUnknownDeprecated(&mf, "geoip", r.GeoIP) + addUnknownDeprecated(&mf, "source_geoip", r.SourceGeoIP) + if r.ClashMode != "" { + mf.unknowns = append(mf.unknowns, condKV{"clash_mode", r.ClashMode}) + } + if r.IPVersion != 0 { + mf.unknowns = append(mf.unknowns, condKV{"ip_version", intStr(r.IPVersion)}) + } + if r.NetworkIsExpensive { + mf.unknowns = append(mf.unknowns, condKV{"network_is_expensive", "true"}) + } + if r.NetworkIsConstrained { + mf.unknowns = append(mf.unknowns, condKV{"network_is_constrained", "true"}) + } + if len(r.NetworkType) > 0 { + mf.unknowns = append(mf.unknowns, condKV{"network_type", interfaceTypes(r.NetworkType)}) + } + return mf +} + +func fieldsFromDNS(r option.RawDefaultDNSRule) matchFields { + mf := matchFields{ + domain: r.Domain, + domainSuffix: r.DomainSuffix, + domainKeyword: r.DomainKeyword, + domainRegex: r.DomainRegex, + srcIPCIDR: r.SourceIPCIDR, + srcIPIsPriv: r.SourceIPIsPrivate, + port: r.Port, + portRange: r.PortRange, + srcPort: r.SourcePort, + srcPortRange: r.SourcePortRange, + network: r.Network, + queryType: r.QueryType, + ruleSet: r.RuleSet, + rsMatchSource: r.RuleSetIPCIDRMatchSource || r.Deprecated_RulesetIPCIDRMatchSource, + invert: r.Invert, + } + // DNS ip_cidr / ip_is_private / ip_accept_any and response_* are response + // filters, not query-routing conditions. + if len(r.IPCIDR) > 0 { + mf.dnsFilter = append(mf.dnsFilter, condKV{"ip_cidr", joinVals(r.IPCIDR)}) + } + if r.IPIsPrivate { + mf.dnsFilter = append(mf.dnsFilter, condKV{"ip_is_private", "true"}) + } + if r.IPAcceptAny { + mf.dnsFilter = append(mf.dnsFilter, condKV{"ip_accept_any", "true"}) + } + if r.ResponseRcode != nil { + mf.dnsFilter = append(mf.dnsFilter, condKV{"response_rcode", "set"}) + } + if r.MatchResponse != nil { + mf.dnsFilter = append(mf.dnsFilter, condKV{"match_response", "set"}) + } + addUnknownList(&mf, "inbound", r.Inbound) + addUnknownList(&mf, "protocol", r.Protocol) + addUnknownList(&mf, "auth_user", r.AuthUser) + addUnknownList(&mf, "user", r.User) + addUnknownList(&mf, "outbound", r.Outbound) + addUnknownList(&mf, "process_name", r.ProcessName) + addUnknownList(&mf, "process_path", r.ProcessPath) + addUnknownList(&mf, "package_name", r.PackageName) + addUnknownList(&mf, "wifi_ssid", r.WIFISSID) + addUnknownList(&mf, "wifi_bssid", r.WIFIBSSID) + addUnknownDeprecated(&mf, "geosite", r.Geosite) + if r.ClashMode != "" { + mf.unknowns = append(mf.unknowns, condKV{"clash_mode", r.ClashMode}) + } + if r.IPVersion != 0 { + mf.unknowns = append(mf.unknowns, condKV{"ip_version", intStr(r.IPVersion)}) + } + return mf +} + +func fieldsFromHeadless(r option.DefaultHeadlessRule) matchFields { + mf := matchFields{ + domainKeyword: r.DomainKeyword, + domainRegex: r.DomainRegex, + srcIPCIDR: r.SourceIPCIDR, + port: r.Port, + portRange: r.PortRange, + srcPort: r.SourcePort, + srcPortRange: r.SourcePortRange, + network: r.Network, + queryType: r.QueryType, + invert: r.Invert, + } + // Prefer pre-compiled matchers (present in binary .srs rule sets). + if r.DomainMatcher != nil { + mf.rawDomain = r.DomainMatcher + } else { + mf.domain = r.Domain + mf.domainSuffix = r.DomainSuffix + } + if r.IPSet != nil { + mf.rawIPSet = r.IPSet + } else { + mf.ipCIDR = r.IPCIDR + } + if r.AdGuardDomainMatcher != nil || len(r.AdGuardDomain) > 0 { + mf.unknowns = append(mf.unknowns, condKV{"adguard_domain", "«set»"}) + } + addUnknownList(&mf, "process_name", r.ProcessName) + addUnknownList(&mf, "process_path", r.ProcessPath) + addUnknownList(&mf, "package_name", r.PackageName) + addUnknownList(&mf, "wifi_ssid", r.WIFISSID) + addUnknownList(&mf, "wifi_bssid", r.WIFIBSSID) + if r.NetworkIsExpensive { + mf.unknowns = append(mf.unknowns, condKV{"network_is_expensive", "true"}) + } + if r.NetworkIsConstrained { + mf.unknowns = append(mf.unknowns, condKV{"network_is_constrained", "true"}) + } + if len(r.NetworkType) > 0 { + mf.unknowns = append(mf.unknowns, condKV{"network_type", interfaceTypes(r.NetworkType)}) + } + return mf +} + +func addUnknownList(mf *matchFields, field string, v badoption.Listable[string]) { + if len(v) > 0 { + mf.unknowns = append(mf.unknowns, condKV{field, joinVals(v)}) + } +} + +func addUnknownDeprecated(mf *matchFields, field string, v badoption.Listable[string]) { + if len(v) > 0 { + mf.unknowns = append(mf.unknowns, condKV{field + " (deprecated/removed)", joinVals(v)}) + } +} + +func interfaceTypes(v badoption.Listable[option.InterfaceType]) string { + parts := make([]string, 0, len(v)) + for _, t := range v { + parts = append(parts, string(t)) + } + return strings.Join(parts, ", ") +} + +func intStr(i int) string { return joinVals([]string{itoa(i)}) } + +func itoa(i int) string { + if i == 0 { + return "0" + } + neg := i < 0 + if neg { + i = -i + } + var b [20]byte + pos := len(b) + for i > 0 { + pos-- + b[pos] = byte('0' + i%10) + i /= 10 + } + if neg { + pos-- + b[pos] = '-' + } + return string(b[pos:]) +} + +// ---- rule-node evaluation (conditions only; action handled by caller) ---- + +// evalRuleNode evaluates a route/DNS rule's match conditions recursively. +func (ec *evalCtx) evalRuleNode(r option.Rule, dns bool) RuleEval { + if r.Type == "logical" { + return ec.evalLogical(r.LogicalOptions.Mode, r.LogicalOptions.Rules, r.LogicalOptions.Invert, dns) + } + var mf matchFields + if dns { + // A DNS rule's default variant is carried on a separate type; caller + // passes route-shaped rules only via evalDNSRuleNode. This branch is for + // route rules. + } + mf = fieldsFromRoute(r.DefaultOptions.RawDefaultRule) + status, conds := ec.evalFields(mf) + return RuleEval{ + Type: "default", + Status: status, + Invert: mf.invert, + Conditions: conds, + Summary: summarize(conds, mf.invert), + } +} + +// evalDNSRuleNode evaluates a DNS rule's match conditions recursively. +func (ec *evalCtx) evalDNSRuleNode(r option.DNSRule) RuleEval { + if r.Type == "logical" { + return ec.evalLogicalDNS(r.LogicalOptions.Mode, r.LogicalOptions.Rules, r.LogicalOptions.Invert) + } + mf := fieldsFromDNS(r.DefaultOptions.RawDefaultDNSRule) + status, conds := ec.evalFields(mf) + return RuleEval{ + Type: "default", + Status: status, + Invert: mf.invert, + Conditions: conds, + Summary: summarize(conds, mf.invert), + } +} + +func (ec *evalCtx) evalLogical(mode string, rules []option.Rule, invert bool, dns bool) RuleEval { + if mode == "" { + mode = "and" + } + var subs []RuleEval + var statuses []string + for _, sub := range rules { + se := ec.evalRuleNode(sub, dns) + subs = append(subs, se) + statuses = append(statuses, se.Status) + } + var status string + if mode == "or" { + status = orStatus(statuses) + } else { + status = andStatus(statuses) + } + if invert { + status = invertStatus(status) + } + return RuleEval{Type: "logical", Mode: mode, Status: status, Invert: invert, Sub: subs, Summary: "logical " + mode} +} + +func (ec *evalCtx) evalLogicalDNS(mode string, rules []option.DNSRule, invert bool) RuleEval { + if mode == "" { + mode = "and" + } + var subs []RuleEval + var statuses []string + for _, sub := range rules { + se := ec.evalDNSRuleNode(sub) + subs = append(subs, se) + statuses = append(statuses, se.Status) + } + var status string + if mode == "or" { + status = orStatus(statuses) + } else { + status = andStatus(statuses) + } + if invert { + status = invertStatus(status) + } + return RuleEval{Type: "logical", Mode: mode, Status: status, Invert: invert, Sub: subs, Summary: "logical " + mode} +} + +func summarize(conds []CondEval, invert bool) string { + if len(conds) == 0 { + return "(match all)" + } + parts := make([]string, 0, len(conds)) + for _, c := range conds { + v := c.Value + if len(v) > 40 { + v = v[:40] + "…" + } + parts = append(parts, c.Field+"="+v) + } + s := strings.Join(parts, " ") + if invert { + s = "NOT(" + s + ")" + } + return s +} diff --git a/internal/engine/ruleset.go b/internal/engine/ruleset.go new file mode 100644 index 0000000..b866bde --- /dev/null +++ b/internal/engine/ruleset.go @@ -0,0 +1,248 @@ +package engine + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/sagernet/sing-box/common/srs" + "github.com/sagernet/sing-box/option" + sjson "github.com/sagernet/sing/common/json" +) + +// ruleSetResolver loads and evaluates rule sets referenced by rules. +type ruleSetResolver struct { + ctx context.Context + byTag map[string]option.RuleSet + files map[string]RuleSetFile + loaded map[string]*loadedRuleSet + warnings *[]string + http *http.Client +} + +type loadedRuleSet struct { + tag string + typ string + rules []option.HeadlessRule + err string +} + +func newRuleSetResolver(ctx context.Context, cfg *Config, files map[string]RuleSetFile, warnings *[]string) *ruleSetResolver { + byTag := map[string]option.RuleSet{} + for _, rs := range cfg.RouteRuleSets { + for _, tag := range rs.Tag { + byTag[tag] = rs + } + } + return &ruleSetResolver{ + ctx: ctx, + byTag: byTag, + files: files, + loaded: map[string]*loadedRuleSet{}, + warnings: warnings, + http: &http.Client{Timeout: 20 * time.Second}, + } +} + +func (r *ruleSetResolver) load(tag string) *loadedRuleSet { + if l, ok := r.loaded[tag]; ok { + return l + } + l := &loadedRuleSet{tag: tag} + r.loaded[tag] = l // set early to avoid cycles + + rs, ok := r.byTag[tag] + if !ok { + l.err = "rule_set not defined in route.rule_set" + return l + } + l.typ = rs.Type + switch rs.Type { + case "inline": + l.rules = rs.InlineOptions.Rules + case "local": + r.loadFromFile(l, rs) + case "remote": + r.loadRemote(l, rs) + default: + l.err = "unsupported rule_set type: " + rs.Type + } + return l +} + +func (r *ruleSetResolver) loadFromFile(l *loadedRuleSet, rs option.RuleSet) { + // Local rule sets read from an on-disk path we don't have; the user uploads + // the file content keyed by the rule-set tag (or its path). + f, ok := r.files[l.tag] + if !ok { + f, ok = r.files[rs.LocalOptions.Path] + } + if !ok { + l.err = fmt.Sprintf("local rule-set file not provided (upload the file for tag %q or path %q)", l.tag, rs.LocalOptions.Path) + return + } + format := f.Format + if format == "" { + format = rs.Format + } + if format == "" { + format = ruleSetFormatFromPath(rs.LocalOptions.Path) + } + data := []byte(f.Data) + if format == "binary" { + if decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(f.Data)); err == nil { + data = decoded + } + } + r.parseInto(l, data, format) +} + +func (r *ruleSetResolver) loadRemote(l *loadedRuleSet, rs option.RuleSet) { + url := rs.RemoteOptions.URL + if url == "" { + l.err = "remote rule-set has no url" + return + } + format := rs.Format + if format == "" { + format = ruleSetFormatFromPath(url) + } + req, err := http.NewRequestWithContext(r.ctx, http.MethodGet, url, nil) + if err != nil { + l.err = err.Error() + return + } + req.Header.Set("User-Agent", "sing-box") + resp, err := r.http.Do(req) + if err != nil { + l.err = "fetch failed: " + err.Error() + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + l.err = fmt.Sprintf("fetch failed: HTTP %d", resp.StatusCode) + return + } + data, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20)) + if err != nil { + l.err = err.Error() + return + } + r.parseInto(l, data, format) +} + +func (r *ruleSetResolver) parseInto(l *loadedRuleSet, data []byte, format string) { + if format == "binary" { + compat, err := srs.Read(bytes.NewReader(data), true) + if err != nil { + l.err = "parse binary rule-set: " + err.Error() + return + } + plain, err := compat.Upgrade() + if err != nil { + l.err = err.Error() + return + } + l.rules = plain.Rules + return + } + // source format + var compat option.PlainRuleSetCompat + if err := sjson.UnmarshalContext(r.ctx, data, &compat); err != nil { + l.err = "parse source rule-set: " + err.Error() + return + } + plain, err := compat.Upgrade() + if err != nil { + l.err = err.Error() + return + } + l.rules = plain.Rules +} + +// evaluate matches a rule-set tag against the context. A rule set matches if ANY +// of its headless rules matches (OR). +func (r *ruleSetResolver) evaluate(tag string, ec *evalCtx, matchSource bool) *RuleSetEval { + l := r.load(tag) + out := &RuleSetEval{Tag: tag, Type: l.typ, MatchedIdx: -1, Count: len(l.rules)} + if l.err != "" { + out.Status = StatusUnknown + out.Error = l.err + return out + } + statuses := make([]string, 0, len(l.rules)) + var firstMatch, firstUnknown *RuleEval + firstMatchIdx := -1 + for i, hr := range l.rules { + re := ec.evalHeadless(hr) + re.Index = i + statuses = append(statuses, re.Status) + if re.Status == StatusMatch && firstMatch == nil { + c := re + firstMatch = &c + firstMatchIdx = i + } + if re.Status == StatusUnknown && firstUnknown == nil { + c := re + firstUnknown = &c + } + } + out.Status = orStatus(statuses) + // Attach a representative headless-rule detail (the decisive one) to keep the + // payload small for large sets. + switch out.Status { + case StatusMatch: + out.MatchedIdx = firstMatchIdx + if firstMatch != nil { + out.Rules = []RuleEval{*firstMatch} + } + case StatusUnknown: + if firstUnknown != nil { + out.Rules = []RuleEval{*firstUnknown} + } + } + return out +} + +// evalHeadless evaluates one headless rule (default or logical). +func (ec *evalCtx) evalHeadless(hr option.HeadlessRule) RuleEval { + if hr.Type == "logical" { + lo := hr.LogicalOptions + mode := lo.Mode + if mode == "" { + mode = "and" + } + var subs []RuleEval + var statuses []string + for _, sub := range lo.Rules { + se := ec.evalHeadless(sub) + subs = append(subs, se) + statuses = append(statuses, se.Status) + } + var status string + if mode == "or" { + status = orStatus(statuses) + } else { + status = andStatus(statuses) + } + if lo.Invert { + status = invertStatus(status) + } + return RuleEval{Type: "logical", Mode: mode, Status: status, Invert: lo.Invert, Sub: subs, Summary: "logical " + mode} + } + mf := fieldsFromHeadless(hr.DefaultOptions) + status, conds := ec.evalFields(mf) + return RuleEval{Type: "default", Status: status, Invert: mf.invert, Conditions: conds, Summary: summarize(conds, mf.invert)} +} + +func ruleSetFormatFromPath(path string) string { + if strings.HasSuffix(strings.ToLower(path), ".srs") { + return "binary" + } + return "source" +} diff --git a/internal/engine/types.go b/internal/engine/types.go new file mode 100644 index 0000000..4c8aef3 --- /dev/null +++ b/internal/engine/types.go @@ -0,0 +1,114 @@ +package engine + +// Tri-state status for a condition / rule evaluation. +const ( + StatusMatch = "match" + StatusNoMatch = "no_match" + StatusUnknown = "unknown" // depends on connection attributes we cannot know offline +) + +// InputTrace is the analysis result for one input line (a domain or IP). +type InputTrace struct { + Input string `json:"input"` + Kind string `json:"kind"` // "domain" | "ip" | "invalid" + Error string `json:"error,omitempty"` + Resolved *ResolvedInfo `json:"resolved,omitempty"` + DNS *DNSTrace `json:"dns,omitempty"` + Route *RouteTrace `json:"route,omitempty"` +} + +// ResolvedInfo holds the DoH resolution result for a domain. +type ResolvedInfo struct { + Server string `json:"server"` + IPv4 []string `json:"ipv4,omitempty"` + IPv6 []string `json:"ipv6,omitempty"` + Error string `json:"error,omitempty"` +} + +// DNSTrace explains which DNS rule (and thus which DNS server / action) a domain +// hits during DNS resolution. +type DNSTrace struct { + QueryType string `json:"queryType"` // the query type used for evaluation (A) + Steps []RuleEval `json:"steps"` + MatchedIndex int `json:"matchedIndex"` // -1 => fell through to final + Final string `json:"final"` // dns.final server tag (or effective default) + Decision *DNSDecision `json:"decision"` + Note string `json:"note,omitempty"` +} + +// DNSDecision is the resolved outcome of DNS routing. +type DNSDecision struct { + ActionType string `json:"actionType"` // route|reject|predefined|route-options|... + Server string `json:"server,omitempty"` + Detail string `json:"detail,omitempty"` + ServerInfo *DNSServerInfo `json:"serverInfo,omitempty"` + FromFinal bool `json:"fromFinal"` // decided by dns.final, not a rule + Assumed bool `json:"assumed"` // decision relied on unknown-condition assumptions +} + +// DNSServerInfo describes a configured DNS server referenced by a route action. +type DNSServerInfo struct { + Tag string `json:"tag"` + Type string `json:"type,omitempty"` + Address string `json:"address,omitempty"` + Detour string `json:"detour,omitempty"` // outbound used to reach this DNS server +} + +// RouteTrace explains which route rule / rule-set a domain or IP hits and the +// final outbound. +type RouteTrace struct { + Steps []RuleEval `json:"steps"` + SelectedIndex int `json:"selectedIndex"` // -1 => fell through to final + Final string `json:"final"` + Decision *RouteDecision `json:"decision"` +} + +// RouteDecision is the resolved outcome of route matching. +type RouteDecision struct { + ActionType string `json:"actionType"` // route|reject|hijack-dns + Outbound string `json:"outbound,omitempty"` + Detail string `json:"detail,omitempty"` + FromFinal bool `json:"fromFinal"` + Assumed bool `json:"assumed"` +} + +// RuleEval is the evaluation of one rule (default or logical) in a rule list. +type RuleEval struct { + Index int `json:"index"` + Type string `json:"type"` // "default" | "logical" + Status string `json:"status"` // match|no_match|unknown + Summary string `json:"summary"` + ActionType string `json:"actionType"` + ActionText string `json:"actionText"` + Terminal bool `json:"terminal"` + Reached bool `json:"reached"` // false for rules after the terminal match (not shown) + Invert bool `json:"invert,omitempty"` + Conditions []CondEval `json:"conditions,omitempty"` + // Logical rule fields. + Mode string `json:"mode,omitempty"` // and|or + Sub []RuleEval `json:"sub,omitempty"` + // Non-terminal side effects (resolve action results, notes). + Effect string `json:"effect,omitempty"` +} + +// CondEval is the evaluation of a single condition within a rule. +type CondEval struct { + Field string `json:"field"` + Value string `json:"value"` + Group string `json:"group"` // dest_addr|src_addr|dest_port|src_port|other|rule_set + Status string `json:"status"` + Matched string `json:"matched,omitempty"` // the specific value that matched, if known + Note string `json:"note,omitempty"` + RuleSet *RuleSetEval `json:"ruleSet,omitempty"` +} + +// RuleSetEval is the evaluation of a referenced rule set. +type RuleSetEval struct { + Tag string `json:"tag"` + Type string `json:"type"` + Status string `json:"status"` + MatchedIdx int `json:"matchedIdx"` // index of the matched headless rule, -1 if none + Rules []RuleEval `json:"rules,omitempty"` + Count int `json:"count"` + Error string `json:"error,omitempty"` +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..9425ed6 --- /dev/null +++ b/run.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Build the WebAssembly engine and serve the static web/ directory. +# +# sing-vis has no backend: this just compiles web/singvis.wasm and starts a +# plain static file server. Any static host works (including GitHub Pages — just +# publish the web/ directory); this script uses Python's http.server for local +# development. Profiles and settings live in the browser (IndexedDB), so there is +# no server-side data directory anymore. +set -euo pipefail +cd "$(dirname "$0")" + +export GOTOOLCHAIN=auto # go.mod needs go >= 1.24.7; auto-fetches the toolchain + +# Accept either "host:port" or a bare port. +ADDR="${1:-127.0.0.1:8787}" +case "$ADDR" in + *:*) HOST="${ADDR%:*}"; PORT="${ADDR##*:}" ;; + *) HOST="127.0.0.1"; PORT="$ADDR" ;; +esac + +./build.sh + +echo "Serving sing-vis (static) on http://${HOST}:${PORT}" +exec python3 -m http.server "${PORT}" --bind "${HOST}" --directory web diff --git a/sing-box b/sing-box new file mode 160000 index 0000000..0b54f57 --- /dev/null +++ b/sing-box @@ -0,0 +1 @@ +Subproject commit 0b54f577251cc7f5e18c52d71f782dd5d2451fda diff --git a/wasmbuild/_stubs/buf_buffer_unix.go b/wasmbuild/_stubs/buf_buffer_unix.go new file mode 100644 index 0000000..8c16bad --- /dev/null +++ b/wasmbuild/_stubs/buf_buffer_unix.go @@ -0,0 +1,11 @@ +// This file is substituted in via `go build -overlay` for the js/wasm build ONLY. +// +// Upstream common/buf/buffer_unix.go (build tag `!windows`) defines +// Buffer.Iovec returning golang.org/x/sys/unix.Iovec, which does not exist for +// GOARCH=wasm. Iovec's only callers are the platform-specific bufio syscall +// paths, none of which are exercised under wasm (there are no raw socket fds in +// a browser). Replacing the file with an empty package body keeps common/buf +// compilable without pulling in unix.Iovec. Native builds do not use the +// overlay and keep the real implementation. + +package buf diff --git a/wasmbuild/_stubs/bufio_copy_direct_posix.go b/wasmbuild/_stubs/bufio_copy_direct_posix.go new file mode 100644 index 0000000..690bc00 --- /dev/null +++ b/wasmbuild/_stubs/bufio_copy_direct_posix.go @@ -0,0 +1,76 @@ +// Substituted in via `go build -overlay` for the js/wasm build ONLY. +// +// Upstream common/bufio/copy_direct_posix.go (build tag `!windows`) implements +// the syscall read-waiters using golang.org/x/sys/unix (readv/recvmsg). wait.go +// (cross-platform) references the createSyscall*ReadWaiter constructors, so the +// symbols must exist for the package to compile. Under wasm there are no raw +// socket fds, so the constructors report "not created" and the read paths are +// never taken. This provides the same identifiers with unix stripped out. + +package bufio + +import ( + "os" + "syscall" + + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ N.ReadWaiter = (*syscallReadWaiter)(nil) + +type syscallReadWaiter struct { + rawConn syscall.RawConn + buffer *buf.Buffer + options N.ReadWaitOptions +} + +func createSyscallReadWaiter(any) (*syscallReadWaiter, bool) { return nil, false } + +func (w *syscallReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + w.options = options + return false +} + +func (w *syscallReadWaiter) WaitReadBuffer() (buffer *buf.Buffer, err error) { + return nil, os.ErrInvalid +} + +var _ N.VectorisedReadWaiter = (*vectorisedSyscallReadWaiter)(nil) + +type vectorisedSyscallReadWaiter struct { + rawConn syscall.RawConn + buffers []*buf.Buffer + options N.ReadWaitOptions +} + +func createVectorisedSyscallReadWaiter(any) (*vectorisedSyscallReadWaiter, bool) { return nil, false } + +func (w *vectorisedSyscallReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + w.options = options + return false +} + +func (w *vectorisedSyscallReadWaiter) WaitReadBuffers() (buffers []*buf.Buffer, err error) { + return nil, os.ErrInvalid +} + +var _ N.PacketReadWaiter = (*syscallPacketReadWaiter)(nil) + +type syscallPacketReadWaiter struct { + rawConn syscall.RawConn + buffer *buf.Buffer + options N.ReadWaitOptions +} + +func createSyscallPacketReadWaiter(any) (*syscallPacketReadWaiter, bool) { return nil, false } + +func (w *syscallPacketReadWaiter) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + w.options = options + return false +} + +func (w *syscallPacketReadWaiter) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { + return nil, M.Socksaddr{}, os.ErrInvalid +} diff --git a/wasmbuild/_stubs/bufio_vectorised_unix.go b/wasmbuild/_stubs/bufio_vectorised_unix.go new file mode 100644 index 0000000..9aff4d7 --- /dev/null +++ b/wasmbuild/_stubs/bufio_vectorised_unix.go @@ -0,0 +1,32 @@ +// Substituted in via `go build -overlay` for the js/wasm build ONLY. +// +// Upstream common/bufio/vectorised_unix.go (build tag `!windows`) implements the +// syscall-based vectorised writers using golang.org/x/sys/unix (Iovec, writev, +// sendmsg). Those types are referenced by the cross-platform vectorised.go, so +// the symbols must exist for the package to compile, but the code never runs +// under wasm (a browser has no raw socket fds). This provides the same +// identifiers with unix stripped out; the write paths return os.ErrInvalid. + +package bufio + +import ( + "os" + + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" +) + +// syscallVectorisedWriterFields is embedded into SyscallVectorisedWriter and +// SyscallVectorisedPacketWriter (declared in vectorised.go). No fields are +// needed for the wasm stub. +type syscallVectorisedWriterFields struct{} + +func (w *SyscallVectorisedWriter) WriteVectorised(buffers []*buf.Buffer) error { + buf.ReleaseMulti(buffers) + return os.ErrInvalid +} + +func (w *SyscallVectorisedPacketWriter) WriteVectorisedPacket(buffers []*buf.Buffer, destination M.Socksaddr) error { + buf.ReleaseMulti(buffers) + return os.ErrInvalid +} diff --git a/wasmbuild/gen-overlay.sh b/wasmbuild/gen-overlay.sh new file mode 100755 index 0000000..a13d218 --- /dev/null +++ b/wasmbuild/gen-overlay.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Generates the -overlay JSON that swaps the three sing source files that pull in +# golang.org/x/sys/unix (unavailable for GOARCH=wasm) for wasm-safe stubs in +# ./_stubs. Only the js/wasm build uses this overlay; native builds and tests are +# unaffected. The stubs live in an underscore-prefixed directory so the go tool +# ignores them during ./... builds (they carry mixed package names). See +# _stubs/*.go for the rationale of each substitution. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +sing="$(cd "$here/.." && go list -m -f '{{.Dir}}' github.com/sagernet/sing)" + +cat < root.querySelector(sel); +const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ( + { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] +)); + +/* ---------------- wasm engine worker ---------------- */ +// The matching engine runs as WebAssembly inside a Web Worker. It is loaded +// lazily on the first Analyze (the wasm is several MB) and reused thereafter. +const engineWorker = (() => { + let worker = null; + let readyPromise = null; + let seq = 0; + const pending = new Map(); + + function ensure() { + if (readyPromise) return readyPromise; + worker = new Worker('worker.js'); + readyPromise = new Promise((resolve, reject) => { + worker.onmessage = (e) => { + const d = e.data || {}; + if (d.type === 'ready') { resolve(); return; } + if (d.type === 'loaderror') { reject(new Error(d.error || 'failed to load engine')); return; } + if (d.id != null && pending.has(d.id)) { + const { resolve: res, reject: rej } = pending.get(d.id); + pending.delete(d.id); + if (d.error) rej(new Error(d.error)); else res(d.result); + } + }; + worker.onerror = (e) => reject(new Error(e.message || 'engine worker error')); + }); + return readyPromise; + } + + return { + // Whether the engine is already initialized (used to tailor the spinner text). + get ready() { return readyPromise !== null; }, + // Kick off loading without running an analysis. + preload() { return ensure(); }, + async analyze(request) { + await ensure(); + const id = ++seq; + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + worker.postMessage({ id, request }); + }); + }, + }; +})(); + +const storage = window.singvisStorage; + +function toast(msg, kind = '') { + const root = $('#toast-root'); + const t = document.createElement('div'); + t.className = 'toast ' + kind; + t.textContent = msg; + root.appendChild(t); + setTimeout(() => { t.style.opacity = '0'; t.style.transition = 'opacity .3s'; setTimeout(() => t.remove(), 300); }, 3600); +} + +const store = { + get(k, def) { try { const v = localStorage.getItem('singvis.' + k); return v == null ? def : JSON.parse(v); } catch { return def; } }, + set(k, v) { try { localStorage.setItem('singvis.' + k, JSON.stringify(v)); } catch {} }, +}; + +/* ---------------- app state ---------------- */ +const state = { + profiles: [], + activeId: null, + settings: { dohServer: 'https://1.1.1.1/dns-query' }, + draft: null, // { id?, name, config, inputs, ruleSetFiles } + lastResult: null, + analyzing: false, +}; + +const SAMPLE_CONFIG = `{ + "dns": { + "servers": [ + { "tag": "proxy-dns", "type": "https", "server": "1.1.1.1", "detour": "proxy" }, + { "tag": "local-dns", "type": "udp", "server": "223.5.5.5", "detour": "direct" } + ], + "rules": [ + { "rule_set": ["geosite-cn"], "action": "route", "server": "local-dns" }, + { "domain_keyword": ["ads"], "action": "reject" } + ], + "final": "proxy-dns" + }, + "route": { + "rules": [ + { "domain_suffix": ["google.com", "openai.com"], "outbound": "proxy" }, + { "rule_set": ["geosite-cn"], "outbound": "direct" }, + { "rule_set": ["geoip-cn"], "outbound": "direct" } + ], + "rule_set": [ + { "type": "remote", "tag": "geosite-cn", "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs" }, + { "type": "remote", "tag": "geoip-cn", "format": "binary", + "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" } + ], + "final": "proxy" + } +}`; + +/* ---------------- init ---------------- */ +async function init() { + $('#btn-settings').onclick = openSettings; + $('#btn-new').onclick = () => newDraft(); + try { + state.settings = await storage.getSettings(); + } catch (e) { /* keep defaults */ } + await loadProfiles(); + if (state.profiles.length) selectProfile(state.profiles[0].id); + else newDraft(); +} + +async function loadProfiles() { + try { state.profiles = (await storage.listProfiles()) || []; } + catch (e) { state.profiles = []; } + renderSidebar(); +} + +function renderSidebar() { + const ul = $('#profile-list'); + if (!state.profiles.length) { + ul.innerHTML = '

  • No profiles yet. Create one to get started.
  • '; + return; + } + ul.innerHTML = state.profiles.map((p) => ` +
  • + ${esc(p.name || 'Untitled')} + updated ${new Date(p.updatedAt).toLocaleString()} +
  • `).join(''); + ul.querySelectorAll('.profile-item').forEach((li) => { + li.onclick = () => selectProfile(li.dataset.id); + }); +} + +async function selectProfile(id) { + try { + const p = await storage.getProfile(id); + state.activeId = id; + state.draft = { id: p.id, name: p.name || '', config: p.config || '', inputs: p.inputs || '', ruleSetFiles: p.ruleSetFiles || {} }; + state.lastResult = null; + renderSidebar(); + renderEditor(); + } catch (e) { toast('Failed to load profile: ' + e.message, 'err'); } +} + +function newDraft() { + state.activeId = null; + state.draft = { name: '', config: SAMPLE_CONFIG, inputs: 'www.google.com\nbaidu.com\nopenai.com\n1.1.1.1', ruleSetFiles: {} }; + state.lastResult = null; + renderSidebar(); + renderEditor(); +} + +/* ---------------- editor ---------------- */ +function renderEditor() { + const d = state.draft; + const network = store.get('network', ''); + const assume = store.get('assumeResolved', true); + const files = Object.entries(d.ruleSetFiles || {}); + $('#content').innerHTML = ` +
    +
    +

    ${state.activeId ? 'Edit profile' : 'New profile'}

    +
    + + ${state.activeId ? '' : ''} +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + ${files.map(([k, f]) => ` +
    + ${esc(k)} + ${esc(f.format)} · ${f.data ? (f.format === 'binary' ? Math.round(f.data.length * 0.75) : f.data.length) : 0} bytes + +
    `).join('')} +
    +
    +
    + + +
    +
    +
    + +
    + + +
    + +
    + + DoH: ${esc(state.settings.dohServer || '—')} +
    + +
    + `; + + $('#btn-save').onclick = saveProfile; + const del = $('#btn-delete'); if (del) del.onclick = deleteProfile; + $('#btn-analyze').onclick = analyze; + $('#f-files').onchange = handleFiles; + $('#opt-assume').onchange = (e) => store.set('assumeResolved', e.target.checked); + $('#opt-network').onchange = (e) => store.set('network', e.target.value); + $('#file-list').querySelectorAll('.rm-file').forEach((b) => { + b.onclick = () => { delete state.draft.ruleSetFiles[b.dataset.key]; syncDraftFromForm(); renderEditor(); }; + }); + if (state.lastResult) renderResults(state.lastResult); +} + +function syncDraftFromForm() { + const d = state.draft; + const n = $('#f-name'); if (n) d.name = n.value; + const c = $('#f-config'); if (c) d.config = c.value; + const i = $('#f-inputs'); if (i) d.inputs = i.value; +} + +function handleFiles(e) { + const files = Array.from(e.target.files || []); + syncDraftFromForm(); + let pending = files.length; + if (!pending) return; + files.forEach((file) => { + const isBinary = /\.srs$/i.test(file.name); + const reader = new FileReader(); + reader.onload = () => { + let data, format; + if (isBinary) { + const bytes = new Uint8Array(reader.result); + let bin = ''; for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); + data = btoa(bin); format = 'binary'; + } else { data = reader.result; format = 'source'; } + state.draft.ruleSetFiles[file.name] = { format, data }; + if (--pending === 0) renderEditor(); + }; + if (isBinary) reader.readAsArrayBuffer(file); else reader.readAsText(file); + }); +} + +async function saveProfile() { + syncDraftFromForm(); + const d = state.draft; + if (!d.name.trim()) { toast('Please enter a profile name', 'err'); return; } + const payload = { name: d.name.trim(), config: d.config, inputs: d.inputs, ruleSetFiles: d.ruleSetFiles }; + if (state.activeId) payload.id = state.activeId; + try { + const saved = await storage.saveProfile(payload); + state.activeId = saved.id; + await loadProfiles(); + await selectProfile(saved.id); + toast('Profile saved', 'ok'); + } catch (e) { toast('Save failed: ' + e.message, 'err'); } +} + +async function deleteProfile() { + if (!state.activeId) return; + if (!confirm('Delete this profile?')) return; + try { + await storage.deleteProfile(state.activeId); + await loadProfiles(); + if (state.profiles.length) selectProfile(state.profiles[0].id); else newDraft(); + toast('Profile deleted', 'ok'); + } catch (e) { toast('Delete failed: ' + e.message, 'err'); } +} + +/* ---------------- analyze ---------------- */ +async function analyze() { + syncDraftFromForm(); + const d = state.draft; + const inputs = d.inputs.split('\n').map((s) => s.trim()).filter(Boolean); + if (!inputs.length) { toast('Add at least one domain or IP', 'err'); return; } + const btn = $('#btn-analyze'); + btn.disabled = true; btn.innerHTML = ' Analyzing…'; + const loadingEngine = !engineWorker.ready; + $('#results').innerHTML = `
    ${loadingEngine ? 'Loading engine (first run, ~a few MB)…' : 'Resolving & matching…'}
    `; + try { + const result = await engineWorker.analyze({ + config: d.config, + inputs, + ruleSetFiles: d.ruleSetFiles, + dohServer: state.settings.dohServer, + network: store.get('network', ''), + assumeResolved: store.get('assumeResolved', true), + }); + state.lastResult = result; + renderResults(result); + } catch (e) { + $('#results').innerHTML = `
    ⚠ ${esc(e.message)}
    `; + } finally { + btn.disabled = false; btn.innerHTML = '▶ Analyze'; + } +} + +/* ---------------- results rendering ---------------- */ +function statusBadge(status) { + const label = { match: 'MATCH', no_match: 'no match', unknown: 'UNKNOWN?' }[status] || status; + return `${label}`; +} + +function renderResults(result) { + const root = $('#results'); + if (!result.inputs || !result.inputs.length) { root.innerHTML = '
    No inputs.
    '; return; } + const warn = (result.warnings && result.warnings.length) + ? `
    ⚠ ${result.warnings.map(esc).join('
    ')}
    ` : ''; + root.innerHTML = warn + result.inputs.map((it, i) => renderInputCard(it, i)).join(''); + root.querySelectorAll('.result-head').forEach((h) => { + h.onclick = () => h.closest('.result-card').classList.toggle('open'); + }); + root.querySelectorAll('.step-head').forEach((h) => { + h.onclick = (e) => { e.stopPropagation(); h.closest('.step').classList.toggle('expanded'); }; + }); +} + +function renderInputCard(it, idx) { + const chips = []; + if (it.kind === 'invalid') { + chips.push(`error${esc(it.error || 'invalid')}`); + } else { + if (it.dns && it.dns.decision) chips.push(dnsChip(it.dns.decision)); + if (it.route && it.route.decision) chips.push(routeChip(it.route.decision)); + } + const open = idx === 0 ? 'open' : ''; + return ` +
    +
    + ${esc(it.input)} + ${esc(it.kind)} +
    ${chips.join('')}
    + +
    +
    + ${it.kind === 'invalid' ? `

    ${esc(it.error || 'Could not parse this input.')}

    ` : ''} + ${renderResolved(it.resolved)} + ${it.dns ? `
    DNS routing (which server / dns action)
    ${renderTrace(it.dns, 'dns')}` : ''} + ${it.route ? `
    Route matching (which rule / outbound)
    ${renderTrace(it.route, 'route')}` : ''} +
    +
    `; +} + +function dnsChip(dec) { + if (dec.actionType === 'reject') return `DNSreject`; + const server = dec.server || (dec.actionType); + const detour = dec.serverInfo && dec.serverInfo.detour ? ` via ${esc(dec.serverInfo.detour)}` : ''; + return `DNS${esc(server)}${detour}`; +} + +function routeChip(dec) { + if (dec.actionType === 'reject') return `routereject`; + if (dec.actionType === 'hijack-dns') return `routehijack-dns`; + return `route${esc(dec.outbound || '(default)')}`; +} + +function renderResolved(r) { + if (!r) return ''; + if (r.error && !(r.ipv4 && r.ipv4.length) && !(r.ipv6 && r.ipv6.length)) { + return `
    Resolved (DoH)

    resolution failed: ${esc(r.error)}

    `; + } + const v4 = (r.ipv4 || []).map((ip) => `${esc(ip)}`).join(''); + const v6 = (r.ipv6 || []).map((ip) => `${esc(ip)}`).join(''); + if (!v4 && !v6) return `
    Resolved (DoH)

    no A/AAAA records

    `; + return `
    Resolved via ${esc(r.server)}
    ${v4}${v6}
    `; +} + +function renderTrace(trace, kind) { + const steps = (trace.steps || []).map((s) => renderStep(s, kind, trace)).join(''); + const stepsHtml = steps ? `
    ${steps}
    ` : `

    No ${kind === 'dns' ? 'DNS' : 'route'} rules — the final is used directly.

    `; + const note = trace.note ? `

    ${esc(trace.note)}

    ` : ''; + return stepsHtml + note + renderDecision(trace.decision, kind); +} + +function renderStep(s, kind, trace) { + const selected = (kind === 'route' && trace.selectedIndex === s.index) || (kind === 'dns' && trace.matchedIndex === s.index); + const cls = ['step', 's-' + s.status]; + if (selected) cls.push('s-selected'); + if (s.status === 'no_match') cls.push('dimmed'); + const conds = s.type === 'logical' ? renderLogical(s) : (s.conditions || []).map(renderCond).join(''); + const effect = s.effect ? `
    ↳ ${esc(s.effect)}
    ` : ''; + const detailInner = conds || 'no conditions (matches all)'; + return ` +
    +
    + ${s.index} + ${statusBadge(s.status)} + ${esc(s.summary)} + ${esc(s.actionText || '')} +
    + ${effect} +
    ${detailInner}
    +
    `; +} + +function renderLogical(s) { + const subs = (s.sub || []).map((sub) => { + const inner = sub.type === 'logical' ? renderLogical(sub) : (sub.conditions || []).map(renderCond).join(''); + return `
    ${statusBadge(sub.status)} ${esc(sub.summary)}
    ${inner}
    `; + }).join(''); + return `
    logical ${esc(s.mode || 'and')}${s.invert ? ' (inverted)' : ''}:
    ${subs}`; +} + +function renderCond(c) { + const matched = c.matched ? `✓ ${esc(c.matched)}` : ''; + const note = c.note ? `
    ${esc(c.note)}
    ` : ''; + const rs = c.ruleSet ? renderRuleSet(c.ruleSet) : ''; + return ` +
    + ${esc(c.field)} ${statusBadge(c.status)} + ${esc(c.group)} + ${esc(c.value)} ${matched} + ${note} +
    ${rs}`; +} + +function renderRuleSet(rs) { + const inner = (rs.rules || []).map((r) => { + const cs = r.type === 'logical' ? renderLogical(r) : (r.conditions || []).map(renderCond).join(''); + return `
    ${statusBadge(r.status)} ${esc(r.summary)}${cs}
    `; + }).join(''); + const err = rs.error ? `
    ⚠ ${esc(rs.error)}
    ` : ''; + const matched = rs.matchedIdx >= 0 ? ` · matched rule #${rs.matchedIdx}` : ''; + return ` +
    +
    ${statusBadge(rs.status)} rule_set ${esc(rs.tag)} + (${esc(rs.type || '?')} · ${rs.count || 0} rules${matched}) +
    + ${err}${inner} +
    `; +} + +function renderDecision(dec, kind) { + if (!dec) return ''; + const assumed = dec.assumed ? `depends on assumptions` : ''; + const fromFinal = dec.fromFinal ? `via ${kind === 'dns' ? 'dns.final' : 'route.final'}` : ''; + if (kind === 'dns') { + const cls = dec.actionType === 'reject' ? 'reject' : 'route'; + let value, sub = ''; + if (dec.actionType === 'reject') { value = 'reject'; } + else if (dec.actionType === 'predefined' || dec.actionType === 'respond') { value = dec.actionType; } + else { + value = dec.server || '(default)'; + if (dec.serverInfo) { + const si = dec.serverInfo; + sub = `${esc(si.type || '')}${si.address ? ' · ' + esc(si.address) : ''}${si.detour ? ' · via outbound ' + esc(si.detour) + '' : ''}`; + } + } + return `
    + DNS action + ${esc(dec.actionType)} + ${esc(value)}${sub} ${fromFinal} ${assumed} +
    `; + } + // route + let cls = 'route', value; + if (dec.actionType === 'reject') { cls = 'reject'; value = 'reject'; } + else if (dec.actionType === 'hijack-dns') { value = 'hijack-dns'; } + else { value = dec.outbound || '(default outbound)'; } + return `
    + Final outbound + ${esc(value)} + ${dec.detail && dec.actionType === 'reject' ? `${esc(dec.detail)}` : ''} + ${fromFinal} ${assumed} +
    `; +} + +/* ---------------- settings ---------------- */ +function openSettings() { + const root = $('#modal-root'); + root.innerHTML = ` + `; + const close = () => { root.innerHTML = ''; }; + $('#s-close').onclick = close; + $('#settings-backdrop').onclick = (e) => { if (e.target.id === 'settings-backdrop') close(); }; + root.querySelectorAll('.preset').forEach((b) => b.onclick = () => { $('#s-doh').value = b.dataset.url; }); + $('#s-save').onclick = async () => { + const dohServer = $('#s-doh').value.trim() || 'https://1.1.1.1/dns-query'; + try { + state.settings = await storage.saveSettings({ dohServer }); + const ind = $('#doh-indicator'); if (ind) ind.textContent = 'DoH: ' + state.settings.dohServer; + close(); toast('Settings saved', 'ok'); + } catch (e) { toast('Save failed: ' + e.message, 'err'); } + }; +} + +init(); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..31d79c6 --- /dev/null +++ b/web/index.html @@ -0,0 +1,42 @@ + + + + + + sing-vis · sing-box route explainer + + + + +
    +
    + +
    +
    sing-vis
    +
    sing-box DNS & route rule explainer
    +
    +
    +
    + +
    +
    + +
    + + +
    +
    + + +
    + + + + + diff --git a/web/storage.js b/web/storage.js new file mode 100644 index 0000000..1a0942e --- /dev/null +++ b/web/storage.js @@ -0,0 +1,138 @@ +'use strict'; + +// IndexedDB-backed persistence for profiles and settings — the browser-side +// replacement for the old file store. IndexedDB (not localStorage) is used +// because uploaded .srs rule-set files can exceed localStorage's ~5MB limit. +// +// Shapes match the former Go store exactly: +// Profile { id, name, config, inputs, createdAt, updatedAt, ruleSetFiles } +// Settings { dohServer } +// +// Exposed as window.singvisStorage (app.js is a classic script). + +(function () { + const DB_NAME = 'singvis'; + const DB_VERSION = 1; + const STORE_PROFILES = 'profiles'; + const STORE_META = 'meta'; + const SETTINGS_KEY = 'settings'; + + const DEFAULT_SETTINGS = { dohServer: 'https://1.1.1.1/dns-query' }; + + let dbPromise = null; + + function openDB() { + if (dbPromise) return dbPromise; + dbPromise = new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains(STORE_PROFILES)) { + db.createObjectStore(STORE_PROFILES, { keyPath: 'id' }); + } + if (!db.objectStoreNames.contains(STORE_META)) { + db.createObjectStore(STORE_META); + } + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error || new Error('failed to open IndexedDB')); + }); + return dbPromise; + } + + // tx runs fn(store) inside a transaction and resolves with `out` (a value fn + // can set) once the transaction completes. + async function tx(storeName, mode, fn) { + const db = await openDB(); + return new Promise((resolve, reject) => { + const t = db.transaction(storeName, mode); + const store = t.objectStore(storeName); + let out; + const ret = (v) => { out = v; }; + Promise.resolve(fn(store, ret)).catch(reject); + t.oncomplete = () => resolve(out); + t.onerror = () => reject(t.error); + t.onabort = () => reject(t.error || new Error('transaction aborted')); + }); + } + + function reqPromise(request) { + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + function newId() { + // Millisecond timestamp plus a short random suffix to avoid collisions when + // several profiles are created within the same millisecond. + return Date.now().toString(36) + Math.random().toString(36).slice(2, 7); + } + + const api = { + async listProfiles() { + const list = await tx(STORE_PROFILES, 'readonly', async (store, ret) => { + ret(await reqPromise(store.getAll())); + }); + const out = list || []; + out.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); + return out; + }, + + async getProfile(id) { + const p = await tx(STORE_PROFILES, 'readonly', async (store, ret) => { + ret(await reqPromise(store.get(id))); + }); + if (!p) throw new Error('profile not found'); + return p; + }, + + // saveProfile creates (when profile.id is empty) or updates a profile, + // assigning id/createdAt/updatedAt the same way the old server did. + async saveProfile(profile) { + const now = Date.now(); + const p = Object.assign({}, profile); + if (!p.id) { + p.id = newId(); + p.createdAt = now; + } + if (!p.createdAt) p.createdAt = now; + p.updatedAt = now; + p.ruleSetFiles = p.ruleSetFiles || {}; + await tx(STORE_PROFILES, 'readwrite', (store) => { + store.put(p); + }); + return p; + }, + + async deleteProfile(id) { + await tx(STORE_PROFILES, 'readwrite', (store) => { + store.delete(id); + }); + }, + + async getSettings() { + const s = await tx(STORE_META, 'readonly', async (store, ret) => { + ret(await reqPromise(store.get(SETTINGS_KEY))); + }); + const merged = Object.assign({}, DEFAULT_SETTINGS, s || {}); + if (!merged.dohServer) merged.dohServer = DEFAULT_SETTINGS.dohServer; + return merged; + }, + + async saveSettings(settings) { + const merged = Object.assign({}, DEFAULT_SETTINGS, settings || {}); + if (!merged.dohServer) merged.dohServer = DEFAULT_SETTINGS.dohServer; + await tx(STORE_META, 'readwrite', (store) => { + store.put(merged, SETTINGS_KEY); + }); + return merged; + }, + + defaultSettings() { + return Object.assign({}, DEFAULT_SETTINGS); + }, + }; + + window.singvisStorage = api; +})(); diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..dc5d21f --- /dev/null +++ b/web/style.css @@ -0,0 +1,282 @@ +:root { + --bg: #f6f7f9; + --bg-elev: #ffffff; + --bg-sunken: #eef0f3; + --border: #e2e5ea; + --border-strong: #cfd4dc; + --text: #1c2128; + --text-dim: #656d78; + --text-faint: #98a0ab; + --accent: #3b6cff; + --accent-weak: #e8eeff; + --match: #1a8a4a; + --match-bg: #e5f5ea; + --nomatch: #8a919b; + --nomatch-bg: #eceef1; + --unknown: #a66a00; + --unknown-bg: #fbefd8; + --reject: #c8321f; + --reject-bg: #fce7e3; + --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; + --radius: 10px; + --shadow: 0 1px 3px rgba(20,25,35,.06), 0 4px 16px rgba(20,25,35,.05); +} +@media (prefers-color-scheme: dark) { + :root { + --bg: #0e1116; + --bg-elev: #161b22; + --bg-sunken: #0b0e13; + --border: #262c36; + --border-strong: #333b47; + --text: #e6edf3; + --text-dim: #9aa4b2; + --text-faint: #6b7683; + --accent: #5b86ff; + --accent-weak: #1a2540; + --match: #4ac57e; + --match-bg: #12291d; + --nomatch: #7d8794; + --nomatch-bg: #1a1f27; + --unknown: #e0a445; + --unknown-bg: #2a2113; + --reject: #f0715c; + --reject-bg: #2c1512; + --shadow: 0 1px 3px rgba(0,0,0,.3), 0 6px 20px rgba(0,0,0,.25); + } +} + +* { box-sizing: border-box; } +html, body { height: 100%; } +body { + margin: 0; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14px; + line-height: 1.5; +} + +/* ---------- top bar ---------- */ +.topbar { + display: flex; align-items: center; justify-content: space-between; + padding: 12px 20px; + background: var(--bg-elev); + border-bottom: 1px solid var(--border); + position: sticky; top: 0; z-index: 20; +} +.brand { display: flex; align-items: center; gap: 12px; } +.logo { + font-size: 26px; color: var(--accent); + width: 40px; height: 40px; display: grid; place-items: center; + background: var(--accent-weak); border-radius: 10px; +} +.title { font-weight: 700; font-size: 17px; letter-spacing: -.2px; } +.subtitle { color: var(--text-dim); font-size: 12px; } + +/* ---------- layout ---------- */ +.layout { display: grid; grid-template-columns: 260px 1fr; min-height: calc(100vh - 65px); } +.sidebar { + border-right: 1px solid var(--border); + background: var(--bg-elev); + padding: 14px 12px; overflow-y: auto; +} +.sidebar-head { + display: flex; align-items: center; justify-content: space-between; + font-weight: 600; font-size: 12px; text-transform: uppercase; + letter-spacing: .5px; color: var(--text-dim); margin-bottom: 10px; padding: 0 4px; +} +.profile-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; } +.profile-item { + padding: 9px 10px; border-radius: 8px; cursor: pointer; + display: flex; flex-direction: column; gap: 2px; border: 1px solid transparent; +} +.profile-item:hover { background: var(--bg-sunken); } +.profile-item.active { background: var(--accent-weak); border-color: var(--accent); } +.profile-item .name { font-weight: 600; } +.profile-item .meta { font-size: 11px; color: var(--text-faint); } +.empty-hint { color: var(--text-faint); font-size: 12px; padding: 8px 4px; } + +.content { padding: 22px 26px; overflow-y: auto; max-width: 1100px; width: 100%; } + +/* ---------- forms ---------- */ +.card { + background: var(--bg-elev); border: 1px solid var(--border); + border-radius: var(--radius); box-shadow: var(--shadow); margin-bottom: 18px; +} +.card-head { + padding: 12px 16px; border-bottom: 1px solid var(--border); + display: flex; align-items: center; justify-content: space-between; gap: 10px; +} +.card-head h2 { margin: 0; font-size: 14px; font-weight: 650; } +.card-body { padding: 16px; } +.field { margin-bottom: 14px; } +.field:last-child { margin-bottom: 0; } +label.lbl { display: block; font-weight: 600; margin-bottom: 6px; font-size: 12.5px; } +label.lbl .hint { font-weight: 400; color: var(--text-faint); margin-left: 6px; font-size: 11.5px; } +input[type=text], input[type=url], textarea, select { + width: 100%; padding: 9px 11px; border: 1px solid var(--border-strong); + border-radius: 8px; background: var(--bg); color: var(--text); + font-family: inherit; font-size: 13.5px; outline: none; +} +input:focus, textarea:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); } +textarea { resize: vertical; font-family: var(--mono); font-size: 12.5px; line-height: 1.55; } +textarea.code { min-height: 180px; white-space: pre; } +textarea.inputs { min-height: 90px; } +.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; } +.row > .field { flex: 1; margin-bottom: 0; min-width: 160px; } +.check { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; } +.check input { width: auto; } + +/* ---------- buttons ---------- */ +.btn { + border: 1px solid var(--border-strong); background: var(--bg-elev); color: var(--text); + padding: 8px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; + font-family: inherit; transition: background .12s, border-color .12s; +} +.btn:hover { background: var(--bg-sunken); } +.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; } +.btn.primary:hover { filter: brightness(1.07); } +.btn.ghost { background: transparent; border-color: transparent; } +.btn.ghost:hover { background: var(--bg-sunken); } +.btn.danger { color: var(--reject); border-color: transparent; background: transparent; } +.btn.danger:hover { background: var(--reject-bg); } +.btn.small { padding: 5px 10px; font-size: 12px; } +.btn:disabled { opacity: .5; cursor: not-allowed; } +.toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; } +.toolbar .spacer { flex: 1; } + +/* ---------- badges ---------- */ +.badge { + display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; + border-radius: 999px; font-size: 11px; font-weight: 700; letter-spacing: .2px; + white-space: nowrap; +} +.badge.kind-domain { background: var(--accent-weak); color: var(--accent); } +.badge.kind-ip { background: var(--bg-sunken); color: var(--text-dim); } +.badge.kind-invalid { background: var(--reject-bg); color: var(--reject); } +.badge.match { background: var(--match-bg); color: var(--match); } +.badge.no_match { background: var(--nomatch-bg); color: var(--nomatch); } +.badge.unknown { background: var(--unknown-bg); color: var(--unknown); } +.badge.reject { background: var(--reject-bg); color: var(--reject); } +.badge.final { background: var(--bg-sunken); color: var(--text-dim); } + +/* ---------- results ---------- */ +.result-card { margin-bottom: 16px; } +.result-head { + display: flex; align-items: center; gap: 12px; padding: 13px 16px; cursor: pointer; + border-bottom: 1px solid transparent; +} +.result-card.open .result-head { border-bottom-color: var(--border); } +.result-head .input-name { font-family: var(--mono); font-weight: 700; font-size: 14px; } +.result-head .caret { color: var(--text-faint); transition: transform .15s; margin-left: auto; } +.result-card.open .result-head .caret { transform: rotate(90deg); } +.outcome-chips { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } +.chip { + display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 8px; + font-size: 12px; background: var(--bg-sunken); border: 1px solid var(--border); +} +.chip .k { color: var(--text-faint); font-weight: 600; } +.chip .v { font-family: var(--mono); font-weight: 700; } +.chip.route .v { color: var(--accent); } +.chip.dns .v { color: var(--text); } +.chip.reject { background: var(--reject-bg); border-color: transparent; } +.chip.reject .v { color: var(--reject); } + +.result-body { padding: 4px 16px 16px; display: none; } +.result-card.open .result-body { display: block; } + +.section-title { + font-size: 12px; text-transform: uppercase; letter-spacing: .6px; color: var(--text-dim); + font-weight: 700; margin: 18px 0 8px; +} +.assume-warn { + background: var(--unknown-bg); color: var(--unknown); border: 1px solid transparent; + padding: 8px 12px; border-radius: 8px; font-size: 12.5px; margin: 6px 0 12px; + display: flex; gap: 8px; align-items: flex-start; +} +.ip-chips { display: flex; flex-wrap: wrap; gap: 6px; } +.ip-chip { font-family: var(--mono); font-size: 12px; background: var(--bg-sunken); border: 1px solid var(--border); padding: 3px 8px; border-radius: 6px; } + +/* trace steps */ +.steps { display: flex; flex-direction: column; gap: 6px; } +.step { + border: 1px solid var(--border); border-radius: 9px; overflow: hidden; background: var(--bg); +} +.step.s-match { border-color: var(--match); } +.step.s-unknown { border-left: 3px solid var(--unknown); } +.step.s-selected { box-shadow: 0 0 0 2px var(--match) inset; } +.step-head { + display: flex; align-items: center; gap: 10px; padding: 8px 12px; cursor: pointer; +} +.step-head:hover { background: var(--bg-sunken); } +.step .idx { + font-family: var(--mono); font-size: 11px; color: var(--text-faint); + min-width: 26px; text-align: center; background: var(--bg-sunken); border-radius: 5px; padding: 1px 4px; +} +.step .summary { font-family: var(--mono); font-size: 12.5px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.step .action-text { font-size: 11.5px; color: var(--text-dim); white-space: nowrap; } +.step .effect { font-size: 11.5px; color: var(--unknown); padding: 0 12px 8px 48px; } +.step-detail { padding: 4px 12px 10px 12px; border-top: 1px dashed var(--border); display: none; } +.step.expanded .step-detail { display: block; } +.step.dimmed { opacity: .62; } + +.cond { + display: grid; grid-template-columns: 130px auto 1fr; gap: 8px; align-items: baseline; + padding: 4px 0; font-size: 12.5px; border-bottom: 1px solid var(--border); +} +.cond:last-child { border-bottom: none; } +.cond .cf { font-weight: 600; font-family: var(--mono); font-size: 12px; } +.cond .cv { font-family: var(--mono); color: var(--text-dim); word-break: break-word; } +.cond .cnote { color: var(--text-faint); font-size: 11.5px; grid-column: 1 / -1; margin-left: 130px; } +.cond .group-tag { font-size: 10px; color: var(--text-faint); text-transform: uppercase; letter-spacing: .3px; } +.cond .matched-val { color: var(--match); font-family: var(--mono); font-size: 11.5px; } + +.ruleset-box { margin: 6px 0 6px 12px; padding: 8px 10px; background: var(--bg-sunken); border-radius: 8px; border: 1px solid var(--border); } +.ruleset-box .rs-head { display: flex; gap: 8px; align-items: center; font-size: 12px; } +.ruleset-box .rs-meta { color: var(--text-faint); font-size: 11px; } +.ruleset-box .rs-err { color: var(--reject); font-size: 11.5px; margin-top: 4px; } + +.decision { + margin-top: 10px; padding: 12px 14px; border-radius: 9px; border: 1px solid var(--border); + background: var(--bg-sunken); display: flex; align-items: center; gap: 12px; flex-wrap: wrap; +} +.decision.route { border-color: var(--accent); background: var(--accent-weak); } +.decision.reject { border-color: var(--reject); background: var(--reject-bg); } +.decision .d-label { font-size: 12px; color: var(--text-dim); font-weight: 700; text-transform: uppercase; letter-spacing: .5px; } +.decision .d-value { font-family: var(--mono); font-weight: 700; font-size: 15px; } +.decision.route .d-value { color: var(--accent); } +.decision.reject .d-value { color: var(--reject); } +.decision .d-sub { font-size: 12px; color: var(--text-dim); } +.arrow { color: var(--text-faint); } + +/* ---------- modal ---------- */ +.modal-backdrop { + position: fixed; inset: 0; background: rgba(10,14,20,.5); display: grid; place-items: center; z-index: 50; +} +.modal { + background: var(--bg-elev); border: 1px solid var(--border); border-radius: 14px; + width: min(560px, 92vw); box-shadow: var(--shadow); max-height: 88vh; overflow: auto; +} +.modal .card-head { position: sticky; top: 0; background: var(--bg-elev); } + +/* ---------- toast ---------- */ +.toast-root { position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 60; } +.toast { + background: var(--bg-elev); border: 1px solid var(--border-strong); border-left: 3px solid var(--accent); + padding: 10px 14px; border-radius: 8px; box-shadow: var(--shadow); font-size: 13px; max-width: 340px; + animation: slidein .2s ease; +} +.toast.err { border-left-color: var(--reject); } +.toast.ok { border-left-color: var(--match); } +@keyframes slidein { from { transform: translateX(20px); opacity: 0; } } + +.muted { color: var(--text-dim); } +.mono { font-family: var(--mono); } +.spinner { display: inline-block; width: 15px; height: 15px; border: 2px solid var(--border-strong); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; vertical-align: -3px; } +@keyframes spin { to { transform: rotate(360deg); } } +.placeholder { text-align: center; color: var(--text-faint); padding: 60px 20px; } +.placeholder .big { font-size: 40px; opacity: .5; margin-bottom: 10px; } +.file-list { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; } +.file-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; background: var(--bg-sunken); padding: 6px 10px; border-radius: 7px; } +.file-row .fname { font-family: var(--mono); font-weight: 600; } +.file-row .fmeta { color: var(--text-faint); font-size: 11px; } diff --git a/web/worker.js b/web/worker.js new file mode 100644 index 0000000..578b912 --- /dev/null +++ b/web/worker.js @@ -0,0 +1,58 @@ +'use strict'; + +// Web Worker that hosts the sing-vis WebAssembly engine. It loads the Go runtime +// shim + singvis.wasm, keeps the Go program alive, and forwards analyze requests +// to the JS-exported `singvisAnalyze`. Running in a worker keeps the wasm work +// (DoH resolution, rule-set fetching, matching) off the UI thread. +// +// Message protocol: +// main → worker: { id, request } request = the analyze payload +// worker → main: { id, result } on success (result = parsed engine.Result) +// { id, error } on failure (error = message string) +// { type:'ready' } once the wasm engine is initialized +// { type:'loaderror', error } if the wasm failed to load + +importScripts('wasm_exec.js'); + +// Resolved by the Go program (via the `singvisReady` callback it invokes in main) +// once `singvisAnalyze` has been registered on the global scope. +let signalStarted; +const started = new Promise((resolve) => { signalStarted = resolve; }); +globalThis.singvisReady = () => signalStarted(); + +const ready = (async () => { + const go = new Go(); + let instance; + try { + // Preferred path; requires the server to send Content-Type: application/wasm. + const res = await WebAssembly.instantiateStreaming(fetch('singvis.wasm'), go.importObject); + instance = res.instance; + } catch (streamErr) { + // Fallback for static servers that don't set the wasm MIME type. + const resp = await fetch('singvis.wasm'); + if (!resp.ok) throw new Error('failed to load singvis.wasm (HTTP ' + resp.status + ')'); + const bytes = await resp.arrayBuffer(); + const res = await WebAssembly.instantiate(bytes, go.importObject); + instance = res.instance; + } + // Do NOT await: the Go main blocks forever (select{}) to keep serving calls. + go.run(instance); + await started; +})(); + +ready.then( + () => self.postMessage({ type: 'ready' }), + (err) => self.postMessage({ type: 'loaderror', error: (err && err.message) || String(err) }) +); + +self.onmessage = async (e) => { + const data = e.data || {}; + if (data.id == null) return; + try { + await ready; + const out = await globalThis.singvisAnalyze(JSON.stringify(data.request || {})); + self.postMessage({ id: data.id, result: JSON.parse(out) }); + } catch (err) { + self.postMessage({ id: data.id, error: (err && err.message) || String(err) }); + } +};