The RpiDNS backend API is a PHP-based REST API that serves the Vue 3 frontend. All data endpoints are dispatched through www/rpi_admin/rpidata.php using a METHOD request_name pattern. Authentication is handled separately by www/rpi_admin/auth.php. RPZ feed management operations delegate to the BindConfigManager class in www/rpi_admin/BindConfigManager.php.
All API responses are JSON. The standard response envelope is:
{
"status": "ok" | "success" | "error" | "failed",
"records": "count",
"data": [ ... ],
"reason": "error description"
}Requests are dispatched in rpidata.php via a switch on $REQUEST['method'] . ' ' . $REQUEST['req']. The $REQUEST array is built by getRequest() (defined in www/rpidns_vars.php), which merges $_REQUEST query/form parameters with any JSON body from php://input and adds the HTTP method.
These parameters are accepted by most list/query endpoints:
| Parameter | Type | Description |
|---|---|---|
req |
string | The request name (endpoint identifier) |
period |
string | Time period: 30m, 1h, 1d, 1w, 30d, custom |
start_dt |
int | Start timestamp (Unix epoch) — required when period=custom |
end_dt |
int | End timestamp (Unix epoch) — required when period=custom |
sortBy |
string | Column to sort by (validated against allowed list) |
sortDesc |
string | true for descending sort |
pp |
int | Per-page limit (1–500, default 100) |
cp |
int | Current page number |
filter |
string | Free-text filter or field=value exact filter |
ltype |
string | Set to stats for aggregated/grouped results |
fields |
string | Comma-separated list of fields to include |
| Method | Request Name | Description |
|---|---|---|
GET |
queries_raw |
Retrieve DNS query log entries |
GET |
hits_raw |
Retrieve RPZ hit (blocked query) log entries |
Both endpoints support all common parameters. The response includes paginated records with a total count:
{
"status": "ok",
"records": "1234",
"data": [
{
"rowid": 1,
"dtz": "2024-01-15T10:30:00Z",
"client_ip": "192.168.1.100",
"mac": "aa:bb:cc:dd:ee:ff",
"fqdn": "example.com",
"type": "A",
"class": "IN",
"options": "+E(0)",
"server": "192.168.1.1",
"action": "allowed",
"cnt": "1",
"cname": "My Device",
"vendor": "Apple",
"comment": ""
}
]
}For hits_raw, the fields differ slightly — type/class/options/server are replaced by rule_type, rule, and feed.
When ltype=stats, results are grouped by unique field combinations with summed counts instead of individual timestamped rows.
All dashboard endpoints accept period (and start_dt/end_dt for custom periods). They return top-N results based on the $dash_topx setting (configured in configuration-files.md).
| Method | Request Name | Description |
|---|---|---|
GET |
dash_topX_req |
Top requested FQDNs (allowed queries) |
GET |
dash_topX_server |
Top DNS servers handling queries |
GET |
dash_topX_req_type |
Top query types (A, AAAA, CNAME, etc.) |
GET |
dash_topX_client |
Top clients by allowed query count |
GET |
dash_topX_breq |
Top blocked FQDNs (RPZ hits) |
GET |
dash_topX_bclient |
Top clients by blocked query count |
GET |
dash_topX_feeds |
Top RPZ feeds by hit count |
Response format:
{
"status": "ok",
"data": [
{ "fname": "example.com", "cnt": 42 }
]
}For client endpoints (dash_topX_client, dash_topX_bclient), the fname field resolves to the asset name if available, falling back to MAC address or IP. A mac field is also included.
| Method | Request Name | Description |
|---|---|---|
GET |
qps_chart |
Queries-per-minute and blocked-per-minute time series |
Returns an ApexCharts-compatible array of two series:
[
{ "name": "Queries", "data": [[1705312200000, 15], [1705312260000, 22]] },
{ "name": "Blocked", "data": [[1705312200000, 3], [1705312260000, 5]] }
]Timestamps are Unix epoch milliseconds. The aggregation granularity depends on the requested period (see Aggregation Tier Selection).
| Method | Request Name | Parameters | Description |
|---|---|---|---|
GET |
assets |
— | List all assets |
POST |
assets |
name, address, vendor, comment |
Create a new asset |
PUT |
assets |
id, name, address, vendor, comment |
Update an asset |
DELETE |
assets |
id |
Delete an asset |
GET response:
{
"status": "ok",
"records": "5",
"data": [
{
"rowid": 1,
"dtz": "2024-01-15T10:30:00Z",
"name": "My Laptop",
"address": "192.168.1.100",
"vendor": "Apple",
"comment": "Work laptop"
}
]
}Both block list and allow list share the same endpoint structure. The req value determines which list is affected: blacklist maps to the block local RPZ zone, whitelist maps to the allow local RPZ zone.
| Method | Request Name | Parameters | Description |
|---|---|---|---|
GET |
blacklist |
— | List all block list entries |
GET |
whitelist |
— | List all allow list entries |
POST |
blacklist |
ioc, active, subdomains, comment |
Add a block list entry |
POST |
whitelist |
ioc, active, subdomains, comment |
Add an allow list entry |
PUT |
blacklist |
id, ioc, active, subdomains, comment |
Update a block list entry |
PUT |
whitelist |
id, ioc, active, subdomains, comment |
Update an allow list entry |
DELETE |
blacklist |
id |
Delete a block list entry |
DELETE |
whitelist |
id |
Delete an allow list entry |
The ioc parameter is validated as a domain name using FILTER_VALIDATE_DOMAIN. When active=true, the IOC is pushed to the BIND DNS server via nsupdate. When subdomains=true, a wildcard entry (*.domain) is also added.
GET response:
{
"status": "ok",
"records": "10",
"data": [
{
"rowid": 1,
"dtz": "2024-01-15T10:30:00Z",
"ioc": "malware.example.com",
"comment": "Known malware domain",
"subdomains": "1",
"active": "1"
}
]
}RPZ feed management endpoints use the BindConfigManager class to parse and modify the BIND configuration file directly.
| Method | Request Name | Parameters | Description |
|---|---|---|---|
GET |
rpz_feeds |
— | List all configured RPZ feeds |
GET |
ioc2rpz_available |
— | Fetch available feeds from ioc2rpz.net API |
POST |
rpz_feed |
feeds[] (JSON body) |
Add one or more RPZ feeds |
PUT |
rpz_feed |
feed, action, description, cnameTarget, primaryServer, tsigKeyName, tsigAlgorithm, tsigKeySecret |
Update a feed's configuration |
DELETE |
rpz_feed |
feed, delete_zone_file |
Remove a feed |
PUT |
rpz_feeds_order |
order[] (JSON body) |
Reorder feeds in response-policy |
PUT |
rpz_feed_status |
feed, enabled |
Enable or disable a feed |
PUT |
retransfer_feed |
feed |
Request zone retransfer (secondary zones only) |
GET rpz_feeds response:
{
"status": "ok",
"records": 6,
"data": [
{
"feed": "allow.ioc2rpz.rpidns",
"action": "passthru",
"desc": "Local allow list",
"source": "local",
"enabled": true,
"order": 1,
"cnameTarget": null,
"primaryServer": null,
"tsigKeyName": null,
"tsigAlgorithm": null
}
]
}All write operations (POST, PUT, DELETE) create a backup of the BIND config before modifying it, validate the result with named-checkconf, and reload BIND via rndc reload. If validation fails, the config is rolled back automatically.
| Method | Request Name | Parameters | Description |
|---|---|---|---|
GET |
RPIsettings |
— | Get current settings and table statistics |
PUT |
RPIsettings |
assets_by, assets_autocreate, hits_raw, hits_5m, hits_1h, hits_1d, queries_raw, queries_5m, queries_1h, queries_1d, dash_topx |
Update settings |
GET response includes per-table record counts, date ranges, sizes, and retention settings:
{
"status": "success",
"retention": [
["queries_raw", 1048576, 50000, "2024-01-01T00:00:00Z", "2024-01-15T23:59:00Z", 14]
],
"assets_by": "mac",
"assets_autocreate": true,
"dashboard_topx": 100
}PUT writes the settings to www/rpisettings.php as a PHP file. Retention values are integers (days). See configuration-files.md for details on the settings file format.
| Method | Request Name | Description |
|---|---|---|
GET |
server_stats |
System health metrics |
Response:
{
"status": "ok",
"records": "4",
"data": [
{ "fname": "CPU load", "cnt": "12.5%, 10.2%, 8.1%" },
{ "fname": "Memory usage", "cnt": "45.2%" },
{ "fname": "Disk usage", "cnt": "32%" },
{ "fname": "Uptime", "cnt": "15 days 3 hours 42 min 10 sec" },
{ "fname": "Temp", "cnt": "42.5'C" }
]
}| Method | Request Name | Parameters | Description |
|---|---|---|---|
GET |
download |
file |
Download a file |
Supported file values:
| Value | File | Content-Type |
|---|---|---|
DB |
SQLite database (gzip compressed) | application/gzip |
CA |
ioc2rpz CA certificate | application/x-pem-file |
bind.log |
BIND general log (gzip) | application/gzip |
bind_queries.log |
BIND query log (gzip) | application/gzip |
bind_rpz.log |
BIND RPZ log (gzip) | application/gzip |
The response is a binary file download (not JSON).
| Method | Request Name | Parameters | Description |
|---|---|---|---|
POST |
import |
file (multipart upload), objects |
Import a database file |
Accepts SQLite, gzip-compressed SQLite, or zip-compressed SQLite files. The uploaded file is extracted to /tmp/rpidns/ and a trigger file (rpidns_import_ready) is written for the import script (scripts/import_db.php) to process.
The objects parameter specifies which data categories to import.
Authentication is handled by www/rpi_admin/auth.php, which defines the AuthService class and exposes its own set of API endpoints.
Source: www/rpi_admin/auth.php
The AuthService class manages user authentication, session tokens, rate limiting, and user administration. It uses the SQLite database for storage.
| Constant | Value | Description |
|---|---|---|
SESSION_DURATION |
86400 (24h) | Session lifetime in seconds |
TOKEN_LENGTH |
32 bytes (64 hex chars) | Cryptographic session token size |
BCRYPT_COST |
12 | bcrypt cost factor for password hashing |
MIN_PASSWORD_LENGTH |
8 | Minimum password length (with complexity) |
PASSPHRASE_LENGTH |
18 | Minimum length for passphrase (no complexity required) |
MAX_LOGIN_ATTEMPTS |
5 | Failed attempts before rate limiting |
RATE_LIMIT_WINDOW |
900 (15min) | Rate limit window in seconds |
Passwords must satisfy one of two policies:
- Standard password (8+ characters): must contain at least one uppercase letter, one lowercase letter, one number, and one symbol.
- Passphrase (18+ characters): no complexity requirements — length alone is sufficient.
- Sessions are token-based using cryptographically secure random tokens (
random_bytes). - Tokens are stored in the
sessionstable with expiration timestamps. - An HTTP-only cookie (
rpidns_session) is set withSameSite=StrictandSecure(when HTTPS). - On password change, all other sessions for the user are invalidated.
- Expired sessions are cleaned up on access.
- Failed login attempts are tracked per IP address in the
login_attemptstable. - After 5 failed attempts within 15 minutes, further login attempts from that IP are blocked.
- Old attempt records are probabilistically cleaned up (1% chance per request).
The AuthService supports verifying passwords hashed with legacy formats from .htpasswd files:
- bcrypt (
$2y$,$2a$,$2b$) - Apache MD5 (
$apr1$) - SHA1 (
{SHA}) - Plain crypt (13-character)
On successful login with a non-bcrypt hash, the password is automatically rehashed to bcrypt.
All auth endpoints are accessed via www/rpi_admin/auth.php with an action parameter.
| Action | Method | Parameters | Auth Required | Admin Required | Description |
|---|---|---|---|---|---|
login |
POST | username, password |
No | No | Authenticate and create session |
logout |
POST | — | No | No | Invalidate current session |
verify |
GET | — | Yes | No | Verify session and return user info |
change_password |
POST | current_password, new_password |
Yes | No | Change own password |
users |
GET | — | Yes | Yes | List all users |
create_user |
POST | username, password, is_admin |
Yes | Yes | Create a new user |
delete_user |
DELETE/POST | user_id |
Yes | Yes | Delete a user |
reset_password |
POST | user_id |
Yes | Yes | Reset a user's password (returns new random password) |
Login response:
{
"status": "success",
"message": "Login successful",
"token": "a1b2c3...",
"user": {
"id": 1,
"username": "admin",
"is_admin": true
},
"expires_at": 1705401600
}The last admin account cannot be deleted (enforced server-side).
Source: www/rpi_admin/BindConfigManager.php
The BindConfigManager class handles all interactions with the BIND DNS server configuration file for RPZ feed management.
The class auto-detects the BIND config file by checking these paths in order:
/etc/bind/named.conf.options/etc/bind/named.conf/etc/named.conf/etc/named/named.conf
An explicit path can be provided via the constructor for testing.
| Source | Description | Zone Type | Example |
|---|---|---|---|
ioc2rpz |
Feeds from ioc2rpz.net | secondary | dga.ioc2rpz |
local |
Locally managed RPZ zones | primary | block.ioc2rpz.rpidns |
third-party |
External RPZ feed providers | secondary | custom-rpz.example.com |
Source type is determined by:
- Names containing
.ioc2rpz(without.rpidns) →ioc2rpz - Names containing
.rpidns→local - Primary/master zone type →
local - Secondary/slave zones with ioc2rpz.net IPs (
94.130.30.123) →ioc2rpz - All other secondary zones →
third-party
Four local RPZ zones are predefined and cannot be deleted:
| Feed | Type | Allowed Actions |
|---|---|---|
allow.ioc2rpz.rpidns |
Allow list | passthru only |
allow-ip.ioc2rpz.rpidns |
Allow list (IP) | passthru only |
block.ioc2rpz.rpidns |
Block list | nxdomain, nodata, drop, cname |
block-ip.ioc2rpz.rpidns |
Block list (IP) | nxdomain, nodata, drop, cname |
| Action | Description |
|---|---|
nxdomain |
Return NXDOMAIN (domain does not exist) |
nodata |
Return NODATA (domain exists but no records) |
passthru |
Allow the query (bypass blocking) |
drop |
Silently drop the query |
cname |
Redirect to a CNAME target |
given |
Use the policy defined in the RPZ zone data |
| Method | Description |
|---|---|
getFeeds() |
Parse config and return all RPZ feeds with order, action, source, enabled state |
addFeeds(array $feeds) |
Add one or more feeds (zone config + response-policy entry) |
updateFeed(string $name, array $config) |
Update a feed's action, description, or server config |
removeFeed(string $name, bool $deleteZoneFile) |
Remove a feed from config (predefined feeds cannot be removed) |
updateFeedOrder(array $order) |
Reorder feeds in the response-policy statement |
setFeedEnabled(string $name, bool $enabled) |
Enable/disable a feed by commenting/uncommenting in response-policy |
retransferZone(string $name) |
Request zone retransfer via rndc retransfer (secondary zones only) |
getTsigKeyName() |
Extract the TSIG key name from config |
getTsigKeyConfig(string $name) |
Get full TSIG key details (name, algorithm, secret) |
backup() |
Create a timestamped backup in /opt/rpidns/backups/bind/ |
restore(string $path) |
Restore config from a backup file |
validate() |
Validate config using named-checkconf |
reloadBind() |
Validate then reload BIND via rndc reload |
- Backups are stored in
/opt/rpidns/backups/bind/with timestamped filenames. - A maximum of 10 backups are retained; older ones are automatically cleaned up.
- Every write operation (add, update, remove, reorder, enable/disable) creates a backup before modifying the config.
- If
named-checkconfvalidation fails after a change, the config is automatically rolled back to the backup.
The class detects whether BIND runs locally or in a Docker container:
- Checks for
/.dockerenvfile - Checks
/proc/1/cgroupfor Docker/LXC indicators - Checks
BIND_CONTAINER_NAMEenvironment variable
For container deployments, rndc commands are executed via docker exec.
The API uses a multi-tier aggregation strategy to balance query performance with data granularity. When a time period is requested, the API selects which database tables to query based on the duration. See database.md for table schemas.
| Period | Duration | Primary Table | Chart Grouping |
|---|---|---|---|
30m |
1,800s | _raw |
per minute |
1h |
3,600s | _5m |
per minute |
1d |
86,400s | _1h |
30-minute buckets |
1w |
604,800s | _1d |
6-hour buckets |
30d |
2,592,000s | _1d |
24-hour buckets |
For period=custom, the tier is selected based on the duration (end_dt - start_dt):
| Duration | Primary Table | Supplementary Tables |
|---|---|---|
| ≤ 1 hour | _raw |
— |
| ≤ 1 day | _5m |
_raw (for data newer than last 5m aggregation) |
| ≤ 7 days | _1h |
_5m + _raw (for recent unaggregated data) |
| > 7 days | _1d |
_1h + _5m + _raw (cascading fill) |
For periods longer than 1 hour, the API uses UNION queries to combine data from multiple tiers. This ensures that recently ingested raw data (not yet aggregated) is included alongside pre-aggregated summaries. Each tier contributes data that is newer than the maximum timestamp in the next-higher aggregation tier.
For example, a 1-day query combines:
queries_rawrows wheredt > max(dt) from queries_5m(unaggregated recent data)queries_5mrows wheredt > max(dt) from queries_1h(5-minute summaries not yet rolled into hourly)queries_1hrows for the full period (hourly summaries)
Source: www/rpi_admin/db_migrate.php
The DbMigration class handles incremental schema upgrades using a versioned migration pattern.
The current schema version is tracked in two places:
PRAGMA user_version— SQLite built-in version pragmaschema_versiontable — records each migration with a timestamp
The target version is defined by the DBVersion constant in www/rpidns_vars.php (currently 2).
getSchemaVersion()reads the current version fromschema_versiontable (falling back toPRAGMA user_version).migrate()runs all pending migrations sequentially fromcurrentVersion + 1totargetVersion.- Each migration runs in a transaction — on failure, the transaction is rolled back.
- Migration methods follow the naming convention
migrateV{from}ToV{to}()(e.g.,migrateV1ToV2()). - After each successful migration, the version is recorded in both
schema_versionandPRAGMA user_version.
| Migration | Description |
|---|---|
migrateV1ToV2 |
Creates authentication tables (users, sessions, login_attempts) with indexes. Imports existing users from .htpasswd if present. Creates a default admin user if no users are imported. |
During the v1→v2 migration, the system attempts to import users from /opt/rpidns/conf/.htpasswd:
- The first imported user is granted admin privileges.
- Password hashes are preserved as-is (bcrypt, Apache MD5, SHA1, crypt) and will be rehashed to bcrypt on first successful login.
- If no
.htpasswdfile exists, a defaultadminuser is created with a random 16-character password written to/opt/rpidns/conf/default_credentials.txt.
The migration can be run manually from the command line:
php www/rpi_admin/db_migrate.phpThe AuthService also triggers migration checks automatically on instantiation.
- Architecture Overview — system architecture and data flow
- Database Schema — table definitions, indexes, and aggregation tiers
- Configuration Files —
rpidns_vars.php,rpisettings.php, and environment variables - Scripts — maintenance scripts including
parse_bind_logs.phpandclean_db.php - BIND Configuration —
named.confstructure and RPZ zone setup - Frontend — Vue 3 components that consume these API endpoints
- Docker Deployment — container configuration, volumes, and deployment procedures
- README — project overview and getting started