Skip to content
API
Endpoints

Endpoints

All endpoints are served at the configured api.listen address (default 127.0.0.1:8080).


GET /api/health/service

Returns the running daemon version, routing runtime status, and resolver configuration summary.

curl http://127.0.0.1:8080/api/health/service

Response

{
  "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.


GET /api/config

Returns the current configuration together with a flag indicating whether it is a staged in-memory draft.

curl http://127.0.0.1:8080/api/config

Response

{
  "config": {
    "daemon": { "pid_file": "/var/run/keen-pbr.pid", "cache_dir": "/var/cache/keen-pbr" },
    "api": { "enabled": true, "listen": "127.0.0.1:8080" },
    "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 daemon 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:8080/api/config \
  -H "Content-Type: application/json" \
  -d @new-config.json

Response

{
  "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 currently staged in-memory config to disk, then applies it to the routing runtime.

curl -X POST http://127.0.0.1:8080/api/config/save

Response

{
  "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:8080/api/runtime/outbounds

Response

{
  "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:8080/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:8080/api/health/routing

Response

{
  "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 passed
  • degraded — one or more checks failed
  • error — an exception prevented checks from completing

Check status values:

  • ok — check passed
  • missing — expected element not found in kernel
  • mismatch — 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.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:8080/api/dns/test

Stream 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}