REST API
This page documents the built-in HTTP REST API.
The REST API is available when:
- Full package version is installed (
keen-pbr, notkeen-pbr-headless) - The config has
api.enabled: true - The
--no-apiflag was not passed at startup
Configuration
{
"device_name": "Home router",
"api": {
"enabled": true,
"listen": "0.0.0.0:12121",
"authentication": {
"enabled": true,
"password_hash": "argon2id$v=19$m=2048,t=2,p=1$..."
},
"cors": {
"allowed_origins": ["https://panel.example.com"]
}
}
}Generate a verifier interactively:
keen-pbr hash-passwordTo enable authentication and write the generated verifier directly to the
configured config.json, use keen-pbr --config /path/to/config.json hash-password --update.
The command updates the file atomically and reminds you to restart the keen-pbr
service because it edits the file outside the running daemon. Authentication
and CORS changes made through the WebUI are persisted and take effect
immediately, without applying a pending routing configuration draft.
device_name is optional and may be empty. When set, the WebUI uses it in the
browser page title and beneath the keen-pbr logo to distinguish devices.
Password verifiers use Monocypher’s Argon2id implementation. Verifiers from
older PBKDF2-based releases are not accepted and must be regenerated.
The config API never returns or accepts the stored verifier. The WebUI reads
only GET /api/auth/password state and sends a new clear-text password only to
the write-only POST /api/auth/settings operation; hashing happens in the daemon.
Authenticated API clients may use Basic authentication with username admin
or a Bearer token returned by POST /api/auth/login. Only one Bearer session is
valid at a time; a successful login immediately invalidates the previous UI
session. Bearer sessions expire after 24 hours and are lost on daemon restart.
When authentication is disabled, cross-origin browser requests are rejected. When enabled, exact configured origins and Chrome/Firefox extension origins are allowed. Since passwords are sent to the API during login or Basic auth, use a trusted network or an HTTPS reverse proxy.
By default, the API listens on 0.0.0.0:12121. All endpoints are served at the configured api.listen address.
GET /api/health/service
Returns the running daemon version, routing runtime status, and resolver configuration summary.
curl http://127.0.0.1:12121/api/health/serviceResponse
{
"version": "3.0.0",
"status": "running",
"resolver_config_hash": "a3f7c1d9e2b84560abcdef1234567890",
"resolver_config_hash_actual": "a3f7c1d9e2b84560abcdef1234567890",
"config_is_draft": false
}resolver_config_hash is an MD5 hex digest of the expected domain-to-ipset mapping derived from the current config. resolver_config_hash_actual reflects the hash of the config that was last applied to the running system resolver. When these two values differ, the dnsmasq config may be out of date.
For live outbound runtime state (health, latency, circuit breaker) use GET /api/runtime/outbounds.
POST /api/lists/refresh
Refreshes remote URL-backed lists from the active daemon config.
- If
nameis provided, only that URL-backed list is refreshed. - If
nameis omitted, all URL-backed lists are refreshed. - If refreshed data changed and affects active routing/DNS while the runtime is running, keen-pbr rebuilds runtime state so updates take effect immediately.
curl -X POST http://127.0.0.1:12121/api/lists/refresh \
-H "Content-Type: application/json" \
-d '{"name":"apple"}'Refresh all URL-backed lists:
curl -X POST http://127.0.0.1:12121/api/lists/refreshRequest Body (optional)
{
"name": "apple"
}name(optional string): List name to refresh.
Response (200)
{
"status": "ok",
"message": "Lists refreshed and runtime reloaded",
"refreshed_lists": ["apple", "google"],
"changed_lists": ["apple"],
"failed_lists": [],
"reloaded": true
}Success payload fields:
refreshed_lists(array[string]): URL-backed lists that were refreshed.changed_lists(array[string]): Refreshed lists whose cached contents changed.failed_lists(array[string]): URL-backed lists that could not be refreshed.reloaded(boolean): Whether the running routing runtime was rebuilt because relevant changed lists were in active use.
Status / Error Behavior
200: Refresh operation completed.400: Requested list exists but is not URL-backed.404: Requested list not found.409: Refresh rejected because a staged draft exists or another config/runtime operation is already in progress.
Error response body:
{
"error": "human-readable message"
}GET /api/config
Returns the current configuration and a flag indicating whether a staged in-memory draft exists.
curl http://127.0.0.1:12121/api/configResponse
{
"config": {
"daemon": { "pid_file": "/var/run/keen-pbr.pid", "cache_dir": "/var/cache/keen-pbr" },
"api": { "enabled": true, "listen": "127.0.0.1:12121" },
"outbounds": [],
"lists": {},
"route": {}
},
"is_draft": false
}is_draft is true when a config has been staged via POST /api/config but not yet saved to disk.
Error Response (500)
{
"error": "Cannot open config file"
}POST /api/config
Validates the provided JSON body as a config file and stages it in memory. The config is not written to disk and the routing runtime is not changed. Use POST /api/config/save to persist and apply the staged draft.
curl -X POST http://127.0.0.1:12121/api/config \
-H "Content-Type: application/json" \
-d @new-config.jsonResponse
{
"status": "ok",
"message": "Config staged in memory"
}Error Response (400 — validation error)
{
"error": "Validation failed",
"validation_errors": [
{ "path": "outbounds.vpn.interface", "message": "interface is required" }
]
}POST /api/config/save
Persists the staged config to disk, then applies it to the routing runtime.
curl -X POST http://127.0.0.1:12121/api/config/saveResponse
{
"status": "ok",
"message": "Config saved and applied",
"saved": true,
"applied": true,
"rolled_back": false
}Error Response (400 — no staged config)
{
"error": "No staged config to save",
"saved": false,
"applied": false,
"rolled_back": false
}GET /api/runtime/outbounds
Returns the daemon’s current outbound runtime state: live urltest selection, interface reachability, and circuit breaker status.
curl http://127.0.0.1:12121/api/runtime/outboundsResponse
{
"outbounds": [
{
"tag": "vpn",
"type": "interface",
"status": "healthy",
"interfaces": [
{ "name": "tun0", "status": "up" }
]
},
{
"tag": "auto_select",
"type": "urltest",
"status": "healthy",
"selected_outbound": "vpn"
}
]
}POST /api/routing/test
Resolves the target (if a domain), scans configured route rules against cached list data to determine the expected outbound, and queries the live kernel firewall sets to determine the actual outbound. Useful for diagnosing routing mismatches without restarting the daemon.
curl -X POST http://127.0.0.1:12121/api/routing/test \
-H "Content-Type: application/json" \
-d '{"target": "example.com"}'Response
{
"target": "example.com",
"is_domain": true,
"resolved_ips": ["93.184.216.34"],
"results": [
{
"ip": "93.184.216.34",
"expected_outbound": "vpn",
"actual_outbound": "vpn",
"ok": true,
"list_match": { "list": "my_domains", "via": "domain" }
}
]
}GET /api/health/routing
Verifies the live kernel routing and firewall state against the expected configuration. Checks that the firewall chain exists, all rules are present, route tables are populated, and policy rules are in place.
curl http://127.0.0.1:12121/api/health/routingResponse
{
"overall": "ok",
"firewall_backend": "nftables",
"firewall": {
"chain_present": true,
"prerouting_hook_present": true,
"detail": "chain keen-pbr found in table mangle"
},
"firewall_rules": [
{
"set_name": "keen-pbr-my_domains",
"action": "MARK",
"expected_fwmark": "0x00010000",
"actual_fwmark": "0x00010000",
"status": "ok"
}
],
"route_tables": [
{
"table_id": 150,
"outbound_tag": "vpn",
"expected_interface": "tun0",
"expected_gateway": "10.8.0.1",
"table_exists": true,
"default_route_present": true,
"interface_matches": true,
"gateway_matches": true,
"status": "ok"
}
],
"policy_rules": [
{
"fwmark": "0x00010000",
"fwmask": "0x00ff0000",
"expected_table": 150,
"priority": 1000,
"rule_present_v4": true,
"rule_present_v6": true,
"status": "ok"
}
]
}Overall status values:
ok— all checks passeddegraded— one or more checks failederror— an exception prevented checks from completing
Check status values:
ok— check passedmissing— expected element not found in kernelmismatch— element found but configuration differs
Error Response (500)
{
"overall": "error",
"error": "failed to connect to netlink socket"
}GET /api/dns/test
Streams DNS queries observed by the built-in dns.dns_test_server listener as Server-Sent Events. Each event payload is a JSON object. The connection receives a HELLO event immediately, then one DNS event per queried name while the connection is open.
curl -N http://127.0.0.1:12121/api/dns/testStream Example
data: {"type":"HELLO"}
data: {"type":"DNS","domain":"example.com","source_ip":"192.168.1.10","ecs":"203.0.113.0/24"}
data: {"type":"DNS","domain":"connectivity-check.local","source_ip":"192.168.1.11","ecs":null}