Skip to content

Server API Reference

The Bifrost server exposes a REST API for management, monitoring, and configuration.

Default Address: http://localhost:7082

Check if the server is healthy and operational.

GET /api/v1/health

Response:

{
"status": "healthy",
"time": "2024-01-15T10:00:00Z"
}
Field Type Description
status string healthy or degraded
time string ISO 8601 timestamp

Get server version information.

GET /api/v1/version

Response:

{
"version": "1.0.0",
"git_commit": "abc123",
"build_time": "2024-01-15T10:00:00Z",
"go_version": "go1.22.0",
"platform": "linux/amd64"
}

Get server running status.

GET /api/v1/status

Response:

{
"status": "running",
"time": "2024-01-15T10:00:00Z",
"version": "1.0.0",
"backends": 3
}

Get aggregated server statistics.

GET /api/v1/stats

Response:

{
"total_connections": 5000,
"active_connections": 25,
"bytes_sent": 104857600,
"bytes_received": 209715200,
"backends": {
"total": 3,
"healthy": 3
},
"time": "2024-01-15T10:00:00Z"
}

Get all configured backends with their status and statistics.

GET /api/v1/backends

Response:

[
{
"name": "direct",
"type": "direct",
"healthy": true,
"stats": {
"active_connections": 5,
"total_connections": 100,
"bytes_sent": 1024000,
"bytes_received": 2048000
}
},
{
"name": "wireguard-us",
"type": "wireguard",
"healthy": true,
"stats": {
"active_connections": 10,
"total_connections": 500,
"bytes_sent": 5120000,
"bytes_received": 10240000
}
}
]

Get a specific backend by name.

GET /api/v1/backends/{name}

Path Parameters:

Parameter Type Description
name string Backend name

Response:

{
"name": "wireguard-us",
"type": "wireguard",
"healthy": true,
"stats": {
"active_connections": 10,
"total_connections": 500,
"bytes_sent": 5120000,
"bytes_received": 10240000
}
}

Add a new backend to the proxy server.

POST /api/v1/backends

Request Body:

{
"name": "my-proxy",
"type": "httpproxy",
"enabled": true,
"httpproxy": {
"address": "proxy.example.com:8080",
"username": "user",
"password": "pass"
}
}

Supported Backend Types:

  • direct - Direct internet connection
  • wireguard - WireGuard VPN tunnel
  • openvpn - OpenVPN tunnel
  • httpproxy - HTTP/HTTPS proxy
  • socks5 - SOCKS5 proxy

Response (201 Created):

{
"status": "created",
"backend": "my-proxy",
"type": "httpproxy"
}

Remove a backend from the proxy server.

DELETE /api/v1/backends/{name}

Response:

{
"status": "removed",
"backend": "my-proxy"
}

Get detailed statistics for a specific backend.

GET /api/v1/backends/{name}/stats

Response:

{
"active_connections": 10,
"total_connections": 500,
"bytes_sent": 5120000,
"bytes_received": 10240000,
"errors": 0,
"latency_ms": 45
}

Test connectivity through a specific backend.

POST /api/v1/backends/{name}/test

Request Body (optional):

{
"target": "google.com:443",
"timeout": "10s"
}

Response:

{
"status": "success",
"backend": "wireguard-us",
"target": "google.com:443",
"duration": "45ms",
"healthy": true
}

Get the current configuration with sensitive values redacted.

GET /api/v1/config

Response: Sanitized ServerConfig object with passwords, keys, and tokens masked.


Get the complete configuration for editing (use with caution).

GET /api/v1/config/full

Response: Full ServerConfig object including sensitive values.


Get metadata about configuration sections, including hot-reload capabilities.

GET /api/v1/config/meta

Response: a JSON array with one entry per top-level config section.

[
{
"section": "routes",
"hot_reloadable": true,
"description": "Routing rules"
},
{
"section": "rate_limit",
"hot_reloadable": true,
"description": "Rate limiting and bandwidth configuration"
},
{
"section": "server",
"hot_reloadable": false,
"description": "Server listeners and timeouts"
},
{
"section": "backends",
"hot_reloadable": false,
"description": "Backend connection configurations"
}
]

Exactly four sections are hot-reloadable: routes, rate_limit (including bandwidth), access_control and cache (rules and presets only — storage changes still require a restart). Every other section reports hot_reloadable: false.

This endpoint is derived from the same table the save path uses, so a client that renders “applies immediately” vs “needs a restart” from it cannot disagree with the save response. Prefer it over a hard-coded client-side list.

One caveat: cache reports hot_reloadable: true, but the reload only touches the cache when a cache manager exists. Turning cache.enabled on from a server that started with caching disabled still needs a restart.


Save configuration changes.

PUT /api/v1/config

Request Body:

{
"config": {
"server": { ... },
"backends": [ ... ],
"routes": [ ... ]
},
"create_backup": true
}

Response:

{
"success": true,
"message": "Configuration saved successfully",
"requires_restart": true,
"changed_sections": ["server", "access_control"],
"hot_reloaded_sections": ["access_control"],
"restart_required_sections": ["server"]
}
Field Type Description
changed_sections string[] Sections that differ from the running config
hot_reloaded_sections string[] Changed sections applied to the running server immediately
restart_required_sections string[] Changed sections written to disk that take effect after a restart
requires_restart boolean True exactly when restart_required_sections is non-empty
backup_path string Path of the backup, when create_backup was set
errors object[] {section, field, message} entries; a reload section means the save succeeded but applying it did not

A save that touches both kinds of section still hot-applies the hot-reloadable part — in the example above the new access_control blocklist is enforced immediately even though the listener change needs a restart. If the reload itself fails, hot_reloaded_sections is empty, every changed section is listed under restart_required_sections, and an error with "section": "reload" is returned; the configuration is still saved to disk.


Validate configuration without saving.

POST /api/v1/config/validate

Request Body: ServerConfig object

Response:

{
"valid": true,
"errors": [],
"warnings": ["Backend 'old-vpn' is enabled but not configured"]
}

Validation covers the auth providers as well as the config shape. An enabled provider whose type cannot authenticate in this build is reported invalid here and refused by PUT /api/v1/config — so a configuration that would fail at the next restart is never saved. Disabled providers are not checked, which keeps “disable it and save” a way out of a bad configuration.

{
"valid": false,
"message": "Configuration validation failed",
"errors": [
{
"section": "auth",
"message": "provider \"corp-ntlm\": auth provider type \"ntlm\" cannot authenticate anyone and is refused: NTLM response verification is not implemented..."
}
]
}

Reload configuration from file (hot-reload for supported sections).

POST /api/v1/config/reload

Response:

{
"message": "Config reloaded successfully",
"time": "2024-01-15T10:00:00Z"
}

Report every auth plugin registered in this server binary, together with whether it can actually authenticate. The Web UI uses this to label providers instead of hard-coding the list, which cannot be correct for build-dependent plugins.

GET /api/v1/auth/plugins

Response:

{
"plugins": [
{
"name": "native",
"type": "native",
"description": "Built-in user database with bcrypt passwords",
"availability": { "state": "available" }
},
{
"name": "system",
"type": "system",
"description": "System/PAM authentication (macOS via dscl; on Linux requires building with CGO_ENABLED=1 -tags pam...)",
"availability": {
"state": "build_disabled",
"reason": "this binary was built without the PAM backend, so system authentication fails closed on Linux..."
}
},
{
"name": "ntlm",
"type": "ntlm",
"description": "NTLM authentication (NON-FUNCTIONAL: fails closed...)",
"availability": {
"state": "unimplemented",
"reason": "NTLM response verification is not implemented: there is no credential source..."
}
}
]
}

Availability states:

State Meaning Configuration
available Works normally Accepted
build_disabled This binary lacks the support the plugin needs (a build tag, cgo, a platform library), so it fails closed here but works in a build that includes it Accepted, with a warning at startup and in the UI
unimplemented Cannot authenticate anyone in any build Refused at config validation

negotiate never appears here: SPNEGO/Negotiate is HTTP middleware configured under auth.negotiate.*, not a provider. A provider of type: negotiate is refused with an error pointing at the correct section.


Get all configured routing rules.

GET /api/v1/routes

Response:

[
{
"name": "internal",
"domains": ["*.internal.company.com", "intranet.*"],
"backend": "wireguard-corp",
"enabled": true,
"priority": 100
},
{
"name": "streaming",
"domains": ["*.netflix.com", "*.youtube.com"],
"backends": ["direct", "wireguard-us"],
"enabled": true,
"priority": 50
}
]

Add a new routing rule.

POST /api/v1/routes

Request Body:

{
"name": "social-media",
"domains": ["*.twitter.com", "*.facebook.com", "*.instagram.com"],
"backend": "direct",
"enabled": true,
"priority": 75
}

Response (201 Created):

{
"status": "created",
"route": "social-media",
"domains": ["*.twitter.com", "*.facebook.com", "*.instagram.com"],
"backend": "direct"
}

Remove a routing rule.

DELETE /api/v1/routes/{name}

Response:

{
"status": "removed",
"route": "social-media"
}

Get recent request log entries (requires enable_request_log: true in config).

GET /api/v1/requests
GET /api/v1/requests?limit=50
GET /api/v1/requests?since=123
GET /api/v1/requests?offset=100

Query Parameters:

Parameter Type Default Description
limit int 100 Maximum entries to return
since int - Get entries since this ID
offset int 0 Skip this many entries

Response:

{
"enabled": true,
"requests": [
{
"id": 1,
"timestamp": "2024-01-15T10:00:00Z",
"method": "GET",
"host": "api.example.com",
"path": "/v1/users",
"url": "https://api.example.com/v1/users",
"user_agent": "Mozilla/5.0...",
"client_ip": "192.168.1.100",
"username": "admin",
"backend": "direct",
"status_code": 200,
"bytes_sent": 1024,
"bytes_recv": 2048,
"duration_ms": 150,
"protocol": "HTTP"
}
]
}

Get request log statistics.

GET /api/v1/requests/stats

Response:

{
"enabled": true,
"count": 1500,
"max_size": 10000,
"total_requests": 48213,
"total_bytes_sent": 10485760,
"total_bytes_recv": 83886080,
"requests_by_method": { "GET": 1200, "POST": 300 },
"requests_by_status": { "200": 1400, "404": 100 },
"top_hosts": [
{ "host": "api.example.com", "count": 900 },
{ "host": "cdn.example.com", "count": 600 }
]
}

Two scopes are mixed in this response, because the request log is a fixed-size ring buffer:

  • total_requests, total_bytes_sent and total_bytes_recv are running totals since the server started or since the log was last cleared. They keep counting after entries are evicted from the buffer.
  • count, requests_by_method, requests_by_status and top_hosts describe only the entries still retained in the buffer — at most max_size of them.

top_hosts is ordered by descending count and holds at most 10 hosts. Every field is always present; the maps and the list are never null.


Clear all request log entries. This also resets total_requests, total_bytes_sent and total_bytes_recv.

DELETE /api/v1/requests

Response:

{
"message": "Request log cleared"
}

Get all currently active connections.

GET /api/v1/connections

Response:

{
"connections": [
{
"id": "20240115100000-1",
"client_ip": "192.168.1.100",
"client_port": "54321",
"host": "api.example.com:443",
"backend": "direct",
"protocol": "CONNECT",
"start_time": "2024-01-15T10:00:00Z",
"bytes_sent": 1024,
"bytes_recv": 2048
}
],
"count": 1,
"time": "2024-01-15T10:00:05Z"
}

Get unique clients with aggregated statistics.

GET /api/v1/connections/clients

Response:

{
"clients": [
{
"client_ip": "192.168.1.100",
"connections": 5,
"bytes_sent": 10240,
"bytes_recv": 20480,
"first_seen": "2024-01-15T09:00:00Z"
}
],
"count": 1,
"time": "2024-01-15T10:00:05Z"
}

Get cache statistics.

GET /api/v1/cache/stats

Response:

{
"enabled": true,
"hit_rate": 0.85,
"total_requests": 12450,
"cache_hits": 10582,
"cache_misses": 1868,
"storage_type": "tiered",
"rules_count": 5,
"memory": {
"entries": 4521,
"size_bytes": 1073741824,
"max_size_bytes": 2147483648
},
"disk": {
"entries": 892,
"size_bytes": 107374182400,
"max_size_bytes": 536870912000
}
}

Get cached entries with optional filtering.

GET /api/v1/cache/entries
GET /api/v1/cache/entries?domain=*.steamcontent.com
GET /api/v1/cache/entries?limit=10&offset=0

Query Parameters:

Parameter Type Default Description
domain string - Filter by domain pattern
limit int 100 Maximum entries (max 1000)
offset int 0 Skip entries for pagination

Response:

{
"entries": [
{
"key": "ab12cd34...",
"url": "http://cdn.steamcontent.com/depot/123/chunk/abc",
"host": "cdn.steamcontent.com",
"size": 1048576,
"content_type": "application/octet-stream",
"created_at": "2024-01-15T10:30:00Z",
"expires_at": "2025-01-15T10:30:00Z"
}
],
"total": 892,
"offset": 0,
"limit": 10
}

Get metadata for a specific cache entry.

GET /api/v1/cache/entries/{key}

Delete a specific cache entry.

DELETE /api/v1/cache/entries/{key}

Response:

{
"message": "Cache entry deleted",
"key": "ab12cd34..."
}

Clear the entire cache.

DELETE /api/v1/cache/entries?confirm=true

Response:

{
"message": "Cache cleared",
"time": "2024-01-15T10:00:00Z"
}

Purge all cache entries for a domain.

DELETE /api/v1/cache/domain/{domain}

Response:

{
"message": "Domain purged from cache",
"domain": "cdn.steamcontent.com",
"deleted": 150
}

Get all caching rules.

GET /api/v1/cache/rules

Response:

{
"rules": [
{
"name": "steam",
"domains": ["*.steamcontent.com", "content*.steampowered.com"],
"enabled": true,
"ttl": "8760h0m0s",
"priority": 100,
"preset": "steam"
}
],
"count": 5
}

Add a custom caching rule.

POST /api/v1/cache/rules

Request Body:

{
"name": "my-cdn",
"domains": ["cdn.example.com", "assets.example.com"],
"enabled": true,
"ttl": "168h",
"priority": 50
}

Response:

{
"message": "Cache rule created",
"rule": "my-cdn"
}

Update an existing cache rule.

PUT /api/v1/cache/rules/{name}

Request Body:

{
"enabled": false
}

Delete a cache rule.

DELETE /api/v1/cache/rules/{name}

Get available caching presets.

GET /api/v1/cache/presets

Response:

{
"presets": [
{
"name": "steam",
"description": "Steam game downloads and updates",
"domains": ["*.steamcontent.com", "content*.steampowered.com"],
"ttl": "8760h0m0s",
"priority": 100
},
{
"name": "epic-games",
"description": "Epic Games Store downloads",
"domains": ["*.epicgames.com", "download.epicgames.com"],
"ttl": "8760h0m0s",
"priority": 100
}
],
"count": 10
}

POST /api/v1/cache/presets/{name}/enable
POST /api/v1/cache/presets/{name}/disable

Response:

{
"message": "Preset enabled",
"preset": "steam"
}

Get all mesh networks.

GET /api/v1/mesh/networks

Response:

{
"networks": [
{
"id": "net-abc123",
"name": "Home Network",
"cidr": "10.100.0.0/24",
"peer_count": 5,
"created": "2024-01-01T00:00:00Z"
}
]
}

Create a new mesh network.

POST /api/v1/mesh/networks

Request Body:

{
"id": "net-xyz789",
"name": "Office Network",
"cidr": "10.200.0.0/24"
}

Response:

{
"id": "net-xyz789",
"name": "Office Network",
"cidr": "10.200.0.0/24",
"peer_count": 0,
"created": "2024-01-15T10:00:00Z"
}

Get details of a specific mesh network.

GET /api/v1/mesh/networks/{networkID}

Delete a mesh network.

DELETE /api/v1/mesh/networks/{networkID}

Register a peer to a mesh network.

POST /api/v1/mesh/networks/{networkID}/peers

Request Body:

{
"network_id": "net-abc123",
"peer": {
"id": "peer-xyz789",
"name": "laptop-work",
"public_key": "base64-encoded-key",
"endpoints": [
{
"address": "192.168.1.100",
"port": 51820,
"type": "lan",
"priority": 100
}
]
}
}

Response:

{
"success": true,
"virtual_ip": "10.100.0.5",
"peers": [
{
"id": "peer-abc123",
"name": "desktop-home",
"virtual_ip": "10.100.0.2",
"endpoints": [...]
}
]
}

Get all peers in a mesh network.

GET /api/v1/mesh/networks/{networkID}/peers

Response:

{
"peers": [
{
"id": "peer-abc123",
"name": "desktop-home",
"public_key": "cHVibGljLWtleQ==",
"virtual_ip": "10.100.0.2",
"endpoints": [
{ "address": "192.168.1.50", "port": 51820, "type": "lan", "priority": 100 }
],
"metadata": {},
"status": "connected",
"connection_type": "direct",
"latency": 12.5,
"last_seen": "2026-08-29T10:14:02Z",
"joined_at": "2026-08-29T09:58:11Z",
"bytes_sent": 481920,
"bytes_received": 1048576
}
]
}

latency is in milliseconds as a number, not a Go duration — a value of 12.5 means 12.5 ms. connection_type is omitted while a peer is still being discovered, and latency is omitted until a measurement exists.

GET /api/v1/mesh/networks/{networkID}/peers/{peerID} returns a single object of the same shape.


Get details of a specific peer.

GET /api/v1/mesh/networks/{networkID}/peers/{peerID}

Update peer information.

PATCH /api/v1/mesh/networks/{networkID}/peers/{peerID}

Request Body:

{
"endpoints": [...],
"metadata": {"location": "office"}
}

Remove a peer from the mesh network.

DELETE /api/v1/mesh/networks/{networkID}/peers/{peerID}

Update peer heartbeat (keep-alive). This also renews the peer’s IP lease.

POST /api/v1/mesh/networks/{networkID}/peers/{peerID}/heartbeat
Status Meaning
204 No Content Last-seen updated and the IP lease renewed
404 Not Found Unknown network or peer
409 Conflict The peer has no IP lease left to renew — it expired and the address may already have been reallocated. Re-register to obtain a new address; continuing to heartbeat will not recover it

A 409 is not a transient error. Answering 204 in that situation would tell the peer its address is still valid while the allocator disagreed, which surfaced later as mesh traffic reaching the wrong node.


Bifrost automatically generates PAC (Proxy Auto-Configuration) files based on routing rules.

GET /proxy.pac
GET /wpad.dat

Response (application/x-ns-proxy-autoconfig):

function FindProxyForURL(url, host) {
if (shExpMatch(host, "*.internal.company.com")) {
return "PROXY bifrost.example.com:7080; DIRECT";
}
return "DIRECT";
}

Browser Configuration:

  • macOS: System Preferences → Network → Advanced → Proxies → Automatic Proxy Configuration
  • Windows: Settings → Network → Proxy → Use setup script
  • Firefox: Settings → Network Settings → Automatic proxy configuration URL
  • Chrome: Uses system settings or Proxy SwitchyOmega extension