The rule editor no longer requires writing expr by hand. Conditions are rows of field + operator + values joined with AND or OR, each row negatable, and the generated expression is shown live and validated by the engine before a rule is accepted. Raw expression and YAML editing are still available. Fields cover the connection (protocol, source/destination IP and port), domains (TLS SNI, QUIC SNI, DNS query name), HTTP (host, path, method, User-Agent) and protocol detection. Operators cover CIDR membership, GeoIP countries, GeoSite categories, port ranges, wildcards, substrings and regular expressions. Multiple values in a row are OR-ed, so one row holds a whole domain or country list. Wildcards compile to the cheapest expression that matches them: *.x.com becomes endsWith, x.* startsWith, *ad* contains, and only a star in the middle falls back to a regular expression. Values are validated as they are typed, including a hint when a star is used with an operator that would match it literally. Country and category pickers are backed by the databases the engine actually loaded, via a new GET /api/v1/geo endpoint (cached, loaded on demand) built on new listing methods in the geo package. Country names and flags come from Intl.DisplayNames, so no name table is shipped. Note that the v2geo format has no AS numbers; the provider groups it does contain (cloudflare, google, telegram, ...) are listed alongside the countries. Opening an existing rule parses its expression back into conditions. Anything the builder cannot represent opens in the expression editor with a warning rather than being rewritten. ruleset/expr_test.go pins the canonical expressions the builder generates and compiles them with the real engine, and the devserver now uses the real ruleset compiler so the same errors show up during frontend work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
6.3 KiB
Markdown
136 lines
6.3 KiB
Markdown
# OpenGFW Web UI
|
|
|
|
A Vue 3 + Tailwind CSS dashboard for OpenGFW, built with [shadcn/ui](https://ui.shadcn.com)
|
|
style components on top of [Reka UI](https://reka-ui.com) primitives. It is embedded into
|
|
the OpenGFW binary and served by the `web` package.
|
|
|
|
- **Dashboard** — live counters, traffic chart, verdict/protocol split, top hosts,
|
|
blocked destinations and triggered rules
|
|
- **Events** — real-time feed of verdicts, rule logs and errors with filters and a
|
|
detail view showing raw analyzer properties
|
|
- **Rules** — a visual condition builder, a raw expression editor and a YAML editor,
|
|
all validated by the engine itself; saving writes the rule file and hot reloads the
|
|
running engine
|
|
- **Analyzers** — which analyzers are compiled in and how much traffic each one saw
|
|
- **Settings** — theme (light/dark/system), language (English/中文) and instance info
|
|
|
|
The layout is responsive: a sidebar on desktop, a bottom tab bar and bottom-sheet
|
|
dialogs on phones.
|
|
|
|
## Enabling it
|
|
|
|
```yaml
|
|
# config.yaml
|
|
web:
|
|
enabled: true
|
|
listen: :8080
|
|
secret: your-password-here
|
|
# cert: /path/to/fullchain.pem
|
|
# key: /path/to/privkey.pem
|
|
```
|
|
|
|
If `secret` is empty a random password is generated and printed to the log on startup.
|
|
|
|
## Rule builder
|
|
|
|
Rules are still plain expr expressions in the rule file; the builder is only a way to
|
|
write them without memorising the syntax. Conditions are rows of *field + operator +
|
|
values*, joined with AND or OR, each row negatable:
|
|
|
|
| Field group | Fields |
|
|
| ----------- | --------------------------------------------------------------- |
|
|
| Connection | transport protocol, source/destination IP, source/destination port |
|
|
| Domain | TLS SNI, QUIC SNI, DNS query name |
|
|
| HTTP | Host, path, method, User-Agent |
|
|
| Protocol | detected protocol (any analyzer) |
|
|
|
|
Operators cover the things rules usually need:
|
|
|
|
| Operator | Generated expression |
|
|
| ----------------------- | ---------------------------------------------------------- |
|
|
| domain or subdomain of | `(S == "x.com" \|\| S endsWith ".x.com")` |
|
|
| matches wildcard | `*.x.com` → `endsWith`, `x.*` → `startsWith`, `*ad*` → `contains`, `a.*.c` → `matches` |
|
|
| in CIDR | `cidr(ip.dst, "10.0.0.0/8")`, validated as you type |
|
|
| in GeoIP country | `geoip(ip.dst, "cn")`, picked from the loaded database |
|
|
| in GeoSite category | `geosite(string(.name), "category-ads-all@cn")` |
|
|
| in range | `(port.dst >= 1000 && port.dst <= 2000)` |
|
|
| is / contains / starts / ends / regex | the matching expr operator |
|
|
|
|
Multiple values in one row are OR-ed together, so one row can hold a whole domain or
|
|
country list. The generated expression is shown live and validated by the engine before
|
|
the rule is accepted.
|
|
|
|
Opening an existing rule parses its expression back into conditions. Anything the
|
|
builder cannot represent — hand written expressions, functions like `lookup()` — opens
|
|
in the expression editor with a warning instead of being rewritten.
|
|
|
|
The GeoIP picker lists whatever the configured `geoip.dat` contains: country codes plus,
|
|
with the default Loyalsoldier database, provider groups such as `cloudflare`, `google`
|
|
and `telegram`. Matching by AS number is not something the v2geo data format supports,
|
|
so use those groups or an explicit CIDR list instead.
|
|
|
|
`web/frontend/src/lib/rule/` holds the whole thing: `fields.ts` (catalog), `compile.ts`
|
|
(builder → expr), `parse.ts` (expr → builder) and `validate.ts`. The canonical
|
|
expressions are pinned in `ruleset/expr_test.go`, which compiles them with the real
|
|
engine.
|
|
|
|
## Layout
|
|
|
|
```
|
|
web/
|
|
├── api.go HTTP handlers (JSON API + SSE)
|
|
├── auth.go password login, session tokens
|
|
├── hub.go statistics collection and the live event fan-out
|
|
├── server.go routes, static file serving, public types
|
|
├── embed.go //go:embed of dist
|
|
├── devserver/ standalone server with synthetic data (any OS)
|
|
├── dist/ built UI, embedded into the binary (committed)
|
|
└── frontend/ Vue sources
|
|
```
|
|
|
|
## Development
|
|
|
|
The engine only builds on Linux, so for UI work there is a standalone server that
|
|
feeds the UI synthetic traffic and an in-memory ruleset:
|
|
|
|
```bash
|
|
go run ./web/devserver # http://127.0.0.1:8080, password: opengfw
|
|
```
|
|
|
|
Then, in another terminal, run Vite with hot reload (it proxies `/api` to `:8080`):
|
|
|
|
```bash
|
|
cd web/frontend
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
To produce the embedded build (this is what `make web` runs):
|
|
|
|
```bash
|
|
cd web/frontend && npm run build # writes ../dist
|
|
```
|
|
|
|
`web/dist` is committed so that `go build` works without Node installed. Rebuild it
|
|
whenever you change the frontend.
|
|
|
|
## API
|
|
|
|
All endpoints live under `/api/v1` and return JSON. Except for `login`, every request
|
|
must carry `Authorization: Bearer <token>`; `GET` endpoints also accept the session
|
|
cookie set at login, which is what the `EventSource` connection uses. Mutating
|
|
endpoints only accept the bearer token, which makes them immune to CSRF.
|
|
|
|
| Method | Path | Description |
|
|
| ---------- | -------------------- | -------------------------------------------------- |
|
|
| `POST` | `/login` | exchange the password for a session token |
|
|
| `POST` | `/logout` | invalidate the current session |
|
|
| `GET` | `/info` | version, platform and engine configuration |
|
|
| `GET` | `/meta` | available analyzers, modifiers, actions, functions |
|
|
| `GET` | `/geo` | GeoIP/GeoSite entries for the rule builder pickers |
|
|
| `GET` | `/metrics` | counters, time series and top N lists |
|
|
| `GET` | `/events?limit=` | recent events from the ring buffer |
|
|
| `GET` | `/live` | server-sent events: `event` and `metrics` frames |
|
|
| `GET/PUT` | `/rules` | read / replace the ruleset (`raw` YAML or `rules`) |
|
|
| `POST` | `/rules/validate` | compile without applying; also converts YAML ⇄ rules |
|