Warning
This document may lag the implementation. It is a high-level architectural
reference and parts of it (notably some config snippets, CLI examples, and API
paths) can drift from the code. The authoritative, maintained documentation is
the Starlight site under docs/ (also published to GitHub Pages).
When in doubt, treat the Go source — internal/config/, cmd/, and
internal/api/ — as the source of truth. Key conventions to keep in mind:
the server listens via server.http.listen / server.socks5.listen (host:port
strings, not numeric *_port fields); routing uses top-level routes:;
authentication uses auth.providers: (the legacy auth.mode is rejected at
load time); CLI management subcommands live under the ctl command; and the
REST API is served under the /api/v1 prefix.
License: MIT License Repository: https://github.com/rennerdo30/bifrost-proxy Status: Production-ready open source software
A Go-based proxy system with client-server architecture providing HTTP, HTTPS, and SOCKS5 proxy capabilities with intelligent routing and traffic debugging.
Important
This project is designed for production use. All components follow strict security practices, including structured logging, encrypted tunnels, and modular authentication.
- HTTP, HTTPS (CONNECT), and SOCKS5 proxy protocols
- VPN tunnel integration (WireGuard and OpenVPN)
- Traditional forward proxy support
- Domain-based intelligent routing
- Multiple authentication modes (none, native, system, LDAP, OAuth)
- Traffic debugging and inspection
- Cross-platform support (Windows, macOS, Linux)
- System tray integration (client)
- Docker deployment support (server)
- Production-ready with proper logging and security
- Rate limiting and bandwidth throttling
- Backend health checks with automatic failover
- Load balancing across multiple backends
- Prometheus metrics and Grafana dashboards
- IPv6 / dual-stack networking support
- Graceful shutdown with connection draining
- Hot reload without dropping connections
- IP whitelist/blacklist access control
- Structured access logging (JSON, Apache formats)
Central proxy server that handles the actual routing through different backends.
graph TD
subgraph "Ingress Layers"
H[HTTP Proxy]
HT[HTTPS Tunnel]
S[SOCKS5 Proxy]
end
subgraph "Core Logic"
H & HT & S --> Router[Router / Matcher]
Router -->|Domain Match| Backends
end
subgraph "Egress Backends"
Backends{Backends}
Backends --> D[Direct Connection]
Backends --> WG[WireGuard Tunnel]
Backends --> F[Forward Proxy]
end
subgraph "Management"
CM[Config Manager]
WUI[Web UI / REST API]
CLI[CLI Control]
end
Local proxy that decides what traffic goes to the server vs direct. Includes system tray integration for easy access on all platforms.
graph TD
subgraph "Local Listeners"
H[HTTP Proxy]
HT[HTTPS Tunnel]
S[SOCKS5 Proxy]
end
subgraph "Traffic Handling"
H & HT & S --> Debug[Traffic Debugger]
Debug --> Router[Router / Matcher]
end
subgraph "Routing Decisions"
Router --> Decisions{Target?}
Decisions -->|Bypass| Direct[Direct Connection]
Decisions -->|Tunnel| Server[Bifrost Server]
end
subgraph "User Interface"
CM[Config Manager]
WUI[Web UI]
CLI[CLI]
Tray[Tray Icon]
end
Every config file — server, client and node — is read by internal/config.Load,
which applies two rules before any schema-specific validation:
- Environment expansion. Exactly three forms are recognized in the raw file:
${NAME}(empty string plus a warning whenNAMEis unset),${NAME:-fallback}(the fallback whenNAMEis unset or empty; the fallback is literal and is not itself expanded), and$$(a literal$). A bare$NAMEis not expanded and every other$is literal, so credentials containing a dollar sign are preserved. Expansion is a single pass, so a substituted value is never re-expanded.config.Saveescapes$that would otherwise be read back as a reference, keeping save/load round trips faithful. - Strict keys. Decoding uses
yaml.DecoderwithKnownFields(true). A key with no corresponding setting fails the load, naming every offending key, its line and the enclosing config type. SettingBIFROST_CONFIG_ALLOW_UNKNOWN_KEYS=1downgrades this to a per-key warning as a documented, transitional migration aid; type errors always fail. This applies to startup,config validate, hot reload and the config API.
Note
The schema below mirrors the Go structs in internal/config/server.go.
Listeners are configured with listen (a host:port string), routing uses
the top-level routes: list, and authentication uses auth.providers:.
See configs/server-config.example.yaml for a complete, working example.
# Listeners and lifecycle
server:
http:
listen: ":7080" # HTTP/HTTPS (CONNECT) proxy listen address
read_timeout: "60s" # Inbound request must fully arrive within this
write_timeout: "60s" # Deadline for a single write to the client
idle_timeout: "120s" # Bound on a connection with nothing in flight
max_connections: 0 # 0 = unlimited
# tls: # Optional TLS for the HTTP listener
# enabled: true
# cert_file: "/path/to/cert.pem"
# key_file: "/path/to/key.pem"
socks5:
listen: ":7180" # SOCKS5 proxy listen address
read_timeout: "30s" # Bounds the SOCKS5 handshake
write_timeout: "30s"
idle_timeout: "60s"
max_connections: 0
graceful_period: "30s" # Drain time on shutdown
> [!IMPORTANT]
> The three listener timeouts describe the **inbound** client connection only.
> `read_timeout` is an absolute bound on a request arriving (from the client's
> first byte), `write_timeout` bounds a *single* write to the client so
> streaming responses are not truncated, and `idle_timeout` bounds a connection
> with nothing in flight — including an established `CONNECT` tunnel or SOCKS5
> relay in which neither direction has carried data. Outbound dial timeouts come
> from `network.dial_timeout` or a backend's own `connect_timeout`.
> [!TIP]
> Use environment variables (e.g., `${OAUTH_CLIENT_SECRET}`) for sensitive credentials to avoid committing them to version control.
# Backend definitions. Type-specific settings live under `config:` (a free-form
# map), not under a type-named key.
backends:
# Direct connection (no proxy)
- name: "direct"
type: "direct"
enabled: true
priority: 10
# WireGuard tunnel
- name: "germany"
type: "wireguard"
enabled: true
priority: 5
config:
config_file: "/path/to/germany.conf"
# Optional per-backend health check
health_check:
type: "tcp" # tcp, http, or ping
interval: "30s"
timeout: "5s"
# OpenVPN tunnel
- name: "uk-vpn"
type: "openvpn"
enabled: true
config:
config_file: "/path/to/uk.ovpn"
auth_file: "/path/to/uk-auth.txt" # Optional: username/password file
# Traditional forward HTTP proxy
- name: "us-proxy"
type: "http_proxy"
enabled: true
config:
address: "proxy.example.com:7080"
username: ""
password: ""
# SOCKS5 forward proxy
- name: "socks-proxy"
type: "socks5_proxy"
enabled: true
config:
address: "socks.example.com:7180"
# Routing rules (top-level `routes:`, evaluated by priority; higher first)
routes:
- name: "Crunchyroll via Germany"
domains:
- "*.crunchyroll.com"
- "crunchyroll.com"
backend: "germany"
priority: 100
# Load balancing across multiple backends for one route
- name: "US streaming"
domains:
- "*.netflix.com"
- "*.hulu.com"
backends: ["us-proxy", "us-proxy-2"]
load_balance: "round_robin" # round_robin, least_conn, ip_hash, weighted
priority: 50
# Default route (lowest priority)
- name: "Default"
domains: ["*"]
backend: "direct"
priority: 1
# Authentication: an ordered list of provider plugins (see Section 14/17).
auth:
providers:
- name: "default"
type: "none" # none, native, system, ldap, oauth, apikey, jwt, ...
enabled: true
priority: 1
config: {} # plugin-specific settings
# Rate limiting (flat keys; bandwidth throttling is nested under `bandwidth`)
rate_limit:
enabled: true
requests_per_second: 1000
burst_size: 100
per_ip: true
per_user: false
bandwidth:
enabled: false
upload: "10Mbps" # Per-connection upload cap
download: "100Mbps" # Per-connection download cap
# IP access control
access_control:
whitelist: [] # If non-empty, only these IPs/CIDRs may connect
blacklist: [] # These IPs/CIDRs are always blocked
# Access logging
access_log:
enabled: true
format: "json" # json, apache
output: "stdout" # stdout or a file path
# Logging (built-in size-based rotation for file outputs)
logging:
level: "info" # debug, info, warn, error
format: "json" # json, text
output: "stdout" # stdout, stderr, or a file path
max_size_mb: 0 # Rotate a file output at this size in MB (<= 0 disables rotation)
max_backups: 0 # Rotated files to retain (<= 0 keeps all)
# Metrics, Web UI, and REST API
metrics:
enabled: true
web_ui:
enabled: true
api:
enabled: true
listen: ":7082"
# token: "..." # When set, every /api/v1/* call, the WebSocket and
# the SSE stream require it.
# Browser origins allowed to open /api/v1/ws, in addition to the server's own
# Host (always allowed). Only needed when a reverse proxy rewrites Host.
# A single "*" disables the origin check and warns at startup.
# allowed_origins:
# - "https://bifrost.example.com"
# - "homeassistant.local:8123"Note
The schema below mirrors the Go structs in internal/config/client.go. Local
listeners live under proxy.http / proxy.socks5 with listen (host:port)
strings, and client-side routing uses a top-level routes: list where each
route has an action of direct or server. See
configs/client-config.example.yaml for a complete example.
# Local proxy listeners
proxy:
http:
listen: "127.0.0.1:7380" # Local HTTP/HTTPS proxy
read_timeout: "30s" # inbound request deadline (0 = disabled; the client default)
write_timeout: "30s" # no-progress bound on writes to the local client
idle_timeout: "60s" # bound on a connection with nothing in flight
socks5:
listen: "127.0.0.1:7381" # Local SOCKS5 proxy
# Bifrost server connection
server:
address: "proxy-server.example.com:7080"
protocol: "http" # http or socks5
# username: "user" # Optional auth
# password: "pass"
timeout: "30s"
retry_count: 3
retry_delay: "1s"
# Client-side routing: action is "direct" (bypass server) or "server" (tunnel)
routes:
- domains: ["*.crunchyroll.com", "*.netflix.com"]
action: "server"
priority: 100
- domains: ["localhost", "127.0.0.1", "*.local"]
action: "direct"
priority: 50
# Everything else through the server
- domains: ["*"]
action: "server"
priority: 1
# Traffic debugging
debug:
enabled: true
max_entries: 1000 # Entries kept in memory
capture_body: false # Capture request/response bodies (high memory)
max_body_size: 65536 # Max body bytes captured when capture_body is true
# filter_domains: ["*.example.com"]
# System tray (when running with a GUI environment)
tray:
enabled: trueStandard WireGuard .conf files:
[Interface]
PrivateKey = <base64-encoded-private-key>
Address = 10.0.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = <base64-encoded-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25Standard OpenVPN .ovpn files are supported:
client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert client.crt
key client.key
# or use auth-user-pass for username/password
auth-user-pass
cipher AES-256-GCM
auth SHA256
verb 3
Authentication options:
# Option 1: Credentials in auth file
openvpn:
config_file: "/path/to/config.ovpn"
auth_file: "/path/to/auth.txt" # Contains username on line 1, password on line 2
# Option 2: Inline credentials (not recommended, use env vars)
openvpn:
config_file: "/path/to/config.ovpn"
username: "${OPENVPN_USERNAME}"
password: "${OPENVPN_PASSWORD}"
# Option 3: Certificate-based (no auth file needed)
openvpn:
config_file: "/path/to/config.ovpn" # Contains cert/key pathsNote: The server uses an embedded OpenVPN client library or spawns the openvpn binary (must be installed on the system). For Docker deployments, the OpenVPN client is included in the image.
Provider backends fetch the provider's server list, select a server and generate a WireGuard or OpenVPN configuration for a delegate backend at runtime.
backends:
- name: "proton-ch"
type: "protonvpn"
enabled: true
config:
auth_mode: "manual" # protonvpn only: "manual" (default) or "api"
username: "${VPN_USER}"
password: "${VPN_PASSWORD}"
country: "CH"
protocol: "openvpn" # "openvpn" or "wireguard"
ca_cert: "${VPN_CA_PEM}" # OpenVPN CA certificate (PEM)
tls_auth_key: "" # Optional OpenVPN tls-auth static keyCrypto-material contract
No CA certificate is embedded for nordvpn, mullvad or protonvpn: ca_cert
is mandatory for protocol: openvpn and is validated fail-closed. pia embeds
PIA's published root (ca.rsa.4096.crt) instead, guarded at package
initialization and re-checked against the clock at profile generation.
Validation is applied at backend construction (config load) and again at profile
generation. ca_cert is accepted only if every PEM block is a CERTIFICATE
that parses as X.509, every self-issued certificate's signature verifies under
its own public key, the first certificate is a CA, and the certificate is inside
its validity window. tls_auth_key, when present, must carry 2048 bits of hex
key material inside an OpenVPN static key block and must not be placeholder
material (all-zero, or a single repeated line). No provider path emits generated
or imported material without running these checks first — including the inline
<ca> block of a profile imported wholesale — and no path substitutes a weaker
verification mode when material is missing: generation fails with an error
naming the offending field. Material referenced out of line by an imported
profile (a ca <file> directive) remains the operator's responsibility.
ProtonVPN authentication modes
auth_mode |
Credentials | Protocols | Notes |
|---|---|---|---|
manual (default) |
OpenVPN/IKEv2 credentials from the account portal | openvpn |
Requires ca_cert |
api |
Proton account credentials | wireguard |
SRP-6a login; required for WireGuard key registration |
In api mode the client performs Proton's SRP-6a exchange: POST /auth/info
returns the modulus as a PGP clear-signed message whose signature is verified
against Proton's modulus-signing key before use, the verifier is derived per the
account's auth version, and the server proof returned by POST /auth is checked
in constant time before a session is stored. Rejection cases: a modulus that is
not clear-signed or fails signature verification, an unsupported auth version,
an out-of-bounds server ephemeral, or a server proof mismatch.
[2024-01-15 10:30:45.123] [REQ] id=abc123 method=GET host=api.example.com path=/v1/users client=127.0.0.1:54321 route=server
[2024-01-15 10:30:45.456] [RES] id=abc123 status=200 size=1234 duration=333ms
With headers enabled:
[2024-01-15 10:30:45.123] [REQ] id=abc123 method=GET host=api.example.com path=/v1/users
Headers: User-Agent: Mozilla/5.0, Accept: application/json, Authorization: Bearer ***
[2024-01-15 10:30:45.456] [RES] id=abc123 status=200 size=1234 duration=333ms
Headers: Content-Type: application/json, Cache-Control: no-cache
Real-time traffic viewer showing:
- Request list with filtering/search
- Request/response details
- Timing waterfall
- Traffic statistics
GET /api/debug/traffic - Recent traffic log
GET /api/debug/traffic/stream - WebSocket for live traffic
GET /api/debug/traffic/:id - Specific request details
POST /api/debug/clear - Clear traffic log
GET /api/debug/stats - Traffic statistics
Runtime management commands talk to a running server via its REST API and live
under the ctl subcommand.
# Start the server
bifrost-server start
bifrost-server start --config /path/to/server-config.yaml
# Validate a config file without starting
bifrost-server validate --config /path/to/server-config.yaml
# Runtime control (via REST API; `ctl` subcommands)
bifrost-server ctl status
bifrost-server ctl backend list
bifrost-server ctl backend show germany
bifrost-server ctl backend add japan --type wireguard
bifrost-server ctl backend remove japan
bifrost-server ctl backend test germany
bifrost-server ctl rule list
bifrost-server ctl rule add anime --domain "*.crunchyroll.com" --backend germany
bifrost-server ctl rule remove anime
bifrost-server ctl config reload
bifrost-server ctl stats
bifrost-server ctl health
# Service management
bifrost-server service install --config /path/to/config.yaml
bifrost-server service start
bifrost-server service status
bifrost-server service stop
bifrost-server service uninstall
# Update management
bifrost-server update check
bifrost-server update install --channel stableAs with the server, runtime control commands live under ctl and talk to the
running client's REST API.
# Start the client
bifrost-client start
bifrost-client start --config /path/to/client-config.yaml
# Initialize a default config
bifrost-client config init
# Runtime control (via REST API; `ctl` subcommands)
bifrost-client ctl status
bifrost-client ctl routes list
bifrost-client ctl routes test example.com
bifrost-client ctl routes add work --domain "*.company.com"
bifrost-client ctl routes remove work
bifrost-client ctl health
# Debug / traffic inspection
bifrost-client ctl debug tail
bifrost-client ctl debug clear
bifrost-client ctl debug errors
bifrost-client ctl debug export --output traffic.har
# VPN (TUN) mode and split tunneling
bifrost-client ctl vpn status
bifrost-client ctl vpn enable
bifrost-client ctl vpn disable
bifrost-client ctl vpn split list
bifrost-client ctl vpn split add-domain "*.internal.company.com"
# Service management
bifrost-client service install --config /path/to/config.yaml
bifrost-client service start
bifrost-client service status
bifrost-client service stop
bifrost-client service uninstall
# Update management
bifrost-client update check
bifrost-client update install --channel stableAll endpoints are served under the /api/v1 prefix.
GET /api/v1/health - Health check
GET /api/v1/version - Build/version info
GET /api/v1/status - Server status
GET /api/v1/stats - Traffic statistics
GET /api/v1/backends - List backends
POST /api/v1/backends - Add backend
GET /api/v1/backends/{name} - Get backend
DELETE /api/v1/backends/{name} - Remove backend
GET /api/v1/backends/{name}/stats - Backend statistics
POST /api/v1/backends/{name}/test - Test backend connectivity
GET /api/v1/routes - List routes
POST /api/v1/routes - Add route
... (route management)
GET /api/v1/config - Get config
GET /api/v1/config/full - Get full resolved config
GET /api/v1/config/meta - Config field metadata (reload vs restart)
PUT /api/v1/config - Save config
POST /api/v1/config/validate - Validate config
POST /api/v1/config/reload - Reload config
GET /api/v1/auth/plugins - Registered auth plugins + availability
GET /api/v1/requests - Recent proxied requests
GET /api/v1/requests/stats - Request log totals and top hosts
DELETE /api/v1/requests - Clear the request log (resets the totals)
GET /api/v1/connections - Active connections
GET /api/v1/cache/... - Response cache control (only when cache.enabled)
GET /api/v1/mesh/... - Mesh coordinator (only when mesh.enabled)
POST /api/v1/login - Exchange api.token for a session cookie
POST /api/v1/logout - Destroy the current session
GET /api/v1/ws - WebSocket event stream
When api.token is set, a request authenticates with any one of:
Authorization: Bearer <token>— the normal path for REST clients.?token=<token>— for transports that cannot set headers from a browser (the WebSocket handshake, EventSource). The parameter is stripped from the URL before the request logger runs, so the credential is not written to Bifrost's own log.- A session cookie from
POST /api/v1/login, which exchanges the token for anHttpOnlycookie. A session can only be created by first presenting the correct token, so it is an alternative credential rather than a bypass.
/api/v1/login and /api/v1/logout are only mounted when api.token is set;
without a token the API is unauthenticated and a session would gate nothing.
/api/v1/ws additionally enforces an origin check, because WebSockets are exempt
from the same-origin policy and CORS. An Origin whose host matches the request
Host is always accepted; anything else must be listed in
api.allowed_origins; a request with no Origin header (non-browser clients) is
accepted. Everything else gets HTTP 403.
POST /api/v1/config/validate and PUT /api/v1/config validate the auth
providers against the plugin registry, not just the config shape. An enabled
provider whose plugin can never authenticate is refused, so such a configuration
cannot be saved and then break the next restart. Plugins report one of three
availability states via GET /api/v1/auth/plugins:
| State | Meaning | Configuration |
|---|---|---|
available |
Works normally | Accepted |
build_disabled |
This binary lacks the required build tag / cgo / platform library (e.g. system without -tags pam) |
Accepted, warned about at startup and in the UI |
unimplemented |
Cannot authenticate anyone in any build (e.g. ntlm) |
Refused at validation |
The /api/v1/mesh/* coordinator routes are mounted only when mesh.enabled is
true (the default); with it set to false the paths return 404. When
mesh.state_path is set, coordinator networks and peers are persisted to that
file (atomic write, mode 0600) and restored on startup with peer virtual IPs
re-pinned, so a restart does not renumber a running mesh. With no state_path
the coordinator is in-memory only. The mesh section requires a restart.
All endpoints are served under the /api/v1 prefix.
GET /api/v1/health - Health check
GET /api/v1/status - Client status + server connection
POST /api/v1/connect - Connect to server
POST /api/v1/disconnect - Disconnect
GET /api/v1/servers - List configured servers
POST /api/v1/server/select - Select active server
GET /api/v1/settings - Get settings
POST /api/v1/settings - Update settings
GET /api/v1/routes - List routing rules (and management)
GET /api/v1/debug/... - Traffic debugging (recent traffic, live WebSocket, clear)
GET /api/v1/vpn/... - VPN/TUN mode control and split tunneling
GET /api/v1/cache/... - Client-side cache control
GET /api/v1/mesh/... - Mesh networking
GET /api/v1/logs - Log streaming
GET /api/v1/config - Get config
POST /api/v1/config/reload - Reload config
JSON encoding of configuration. Config structs serialize with the same
snake_case names their YAML keys use, so a response body is a valid request body.
Durations are duration strings in both directions ("30s", "1m30s"), never
nanosecond counts; a bare number is still accepted on input and read as
nanoseconds (an integral float such as 3e+11 counts — old releases persisted
that form — but a fractional nanosecond is an error). There is exactly one
duration contract: internal/config.Duration is an alias of
internal/duration.Duration, which also serves internal/vpn and
internal/mesh.
For compatibility, imports of JSON exports written before the structs carried
json: tags also accept the legacy Go field names (SplitTunnel, CacheTTL,
NetworkID, HeartbeatInterval, …). A document naming both spellings of the
same field with different values is rejected as ambiguous. Output is always
canonical snake_case.
Server:
golang.zx2c4.com/wireguard- WireGuard implementation in Gogithub.com/armon/go-socks5- SOCKS5 server implementationgopkg.in/yaml.v3- YAML parsinggithub.com/gorilla/mux- HTTP routing for APIgithub.com/gorilla/websocket- WebSocket for live updatesgithub.com/spf13/cobra- CLI framework
Client:
- Same as server minus wireguard
- Plus traffic inspection utilities
For full HTTPS debugging (seeing decrypted content), the client needs to:
- Generate a CA certificate
- User installs CA in their trust store
- Client generates per-domain certificates on-the-fly
This is optional and requires explicit user setup. By default, HTTPS is tunneled (CONNECT method) and only metadata is logged.
Domain patterns support:
- Exact match:
example.com - Wildcard subdomain:
*.example.com(matches any.example.com, also example.com itself) - Suffix match:
.example.com(matches example.com and all subdomains) - Full wildcard:
* - Glob patterns within labels:
sf-*.example.com(matches sf-abc.example.com, sf-xyz.example.com)
Glob Pattern Examples:
| Pattern | Matches | Does Not Match |
|---|---|---|
sf-*.example.com |
sf-abc.example.com, sf-123.example.com |
other.example.com |
*-api.example.com |
backend-api.example.com, v2-api.example.com |
api.example.com |
pre-*-suf.example.com |
pre-middle-suf.example.com |
pre-other.example.com |
*.sf-*.example.com |
sub.sf-abc.example.com |
sf-abc.example.com |
Matching is case-insensitive.
sequenceDiagram
participant B as Browser
participant C as Bifrost Client
participant S as Bifrost Server
participant T as Target Target
Note over B, T: Direct Connection
B->>C: Request
C->>T: Connect (Direct)
T-->>C: Response
C-->>B: Response
Note over B, T: Proxy Connection
B->>C: Request
C->>S: Encrypted Tunnel
S->>T: Connect (Backend)
T-->>S: Response
S-->>C: Response
C-->>B: Response
- Server Web UI should be protected (firewall, auth, or localhost-only behind reverse proxy)
- Client binds to localhost by default
- WireGuard .conf files contain private keys - protect file permissions
- MITM mode requires careful handling of generated certificates
The mesh data plane (internal/p2p) is the only path by which a remote host can
get bytes written to the local TUN/TAP device, so its handshake is the trust
boundary.
Wire format. Handshake initiation and response are both a fixed 105 bytes: message type (1) | sender static X25519 public key (32) | sender ephemeral X25519 public key (32) | handshake timestamp (8, little endian) | HMAC-SHA256 authenticator over the preceding 73 bytes (32). Data frames are type (1) | nonce (12) | ChaCha20-Poly1305 ciphertext and tag.
Session keys. staticShared = X25519(static_priv, remote_static_pub)
authenticates the peer and is the HKDF-SHA256 input keying material.
ephemeralShared = X25519(eph_priv, remote_eph_pub) is fresh per handshake and
is the HKDF salt. The HKDF info string is a direction label (send/recv)
followed by the handshake transcript: both static public keys, both ephemeral
public keys in initiator-then-responder order, and the timestamp. Consequences:
- Session keys are unique per handshake, so the frame nonce counter can safely
restart at 0 on reconnect without ever reusing a
(key, nonce)pair. - Ephemeral private keys are discarded with the session, giving forward secrecy.
- The two ends derive matching keys only if they agree on every handshake input, so a tampered, spliced or reflected handshake yields non-matching keys instead of anything an attacker controls.
Nonce invariant. Within one session the send nonce comes only from an atomic increment, and keys are unique per session. Reusing a key across handshakes, or resetting the counter mid-session, would be catastrophic for ChaCha20-Poly1305 — both are called out in the code.
Inbound authorization (fail closed, in order):
- The initiator's static public key must be one this node learned from
discovery, and must appear in
mesh.security.allowed_peerswhen that list is non-empty. Authorization is revoked when the peer leaves the mesh. - The handshake authenticator must verify, proving the sender holds the static private key for the key it claims. Public keys are distributed by discovery and are not secrets, so this is the check that separates the real peer from anyone who merely knows its key.
- The handshake timestamp must exceed the greatest previously accepted for that key, rejecting a replayed initiation. Timestamps are strictly increasing per initiator process. High-water marks are in memory only, so the first initiation after a restart is accepted unconditionally.
Only after all three does the node install the connection, so an unauthorized
peer's frames never reach writeToDevice.
Data-frame replay. Each session carries an RFC 6479-style sliding-window bitmap (2048-bit capacity, 1984-nonce usable window) over authenticated frame nonces. A nonce is accepted at most once, anything below the window floor is rejected, and the floor never moves backwards.
Encryption is not optional. There is no plaintext mesh transport.
mesh.security.require_encryption: false is normalized to true with a warning
rather than honoured.
Caution
WireGuard and OpenVPN configuration files contain sensitive private keys. Ensure file permissions are restricted (e.g., chmod 600) and they are excluded from backups where appropriate.
Both client and server are fully supported on all major platforms:
| Platform | Server | Client | Notes |
|---|---|---|---|
| Linux | Full support | Full support | Best WireGuard performance (kernel module) |
| macOS | Full support | Full support | wireguard-go userspace, native tray support |
| Windows | Full support | Full support | wireguard-go with wintun, native tray support |
- Elevated permissions required for WireGuard tunnel creation
- Can run as systemd service (Linux), launchd (macOS), or Windows Service
- Docker container available for easy deployment
- No special permissions required
- System tray requires GUI environment
- Can run headless (CLI-only mode) if needed
The client includes a system tray icon for easy access on all platforms.
- Status: Shows connection status (connected/disconnected)
- Open Web UI: Opens the debug/management interface in browser
- Enable/Disable Proxy: Quick toggle for proxy
- Quick Rules: Submenu to enable/disable specific routing rules
- View Traffic: Opens traffic debugger
- Settings: Opens settings panel
- Quit: Gracefully stops the client
- Green: Connected and routing through server
- Yellow: Connected but server unreachable
- Gray: Proxy disabled
- Red: Error state
- Server connection established/lost
- Configuration reloaded
- Errors (connection failures, config issues)
- Windows: Uses native Win32 system tray API
- macOS: Uses NSStatusItem (menu bar)
- Linux: Uses libappindicator/StatusNotifierItem (works with GNOME, KDE, etc.)
The server can be deployed as a Docker container.
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o bifrost-server ./cmd/server
FROM alpine:latest
RUN apk add --no-cache iptables openvpn
COPY --from=builder /app/bifrost-server /usr/local/bin/
COPY --from=builder /app/configs/server-config.yaml /etc/bifrost/config.yaml
EXPOSE 7080 7180 7081
ENTRYPOINT ["bifrost-server", "start", "--config", "/etc/bifrost/config.yaml"]version: '3.8'
services:
bifrost-server:
build: .
container_name: bifrost-server
restart: unless-stopped
cap_add:
- NET_ADMIN # Required for WireGuard
sysctls:
- net.ipv4.ip_forward=1
ports:
- "7080:7080" # HTTP/HTTPS proxy
- "7180:7180" # SOCKS5 proxy
- "7081:7081" # Web UI
volumes:
- ./server-config.yaml:/app/data/config.yaml:ro
- bifrost-data:/app/data
- bifrost-logs:/var/log/bifrost
devices:
- /dev/net/tun:/dev/net/tun # TUN device for VPN
environment:
- LOG_LEVEL=info# Build and run
docker-compose up -d
# View logs
docker-compose logs -f
# Reload configuration
docker exec bifrost-server bifrost-server config reload| Type | Description | Use Case |
|---|---|---|
tcp |
TCP connection test | Basic connectivity check |
http |
HTTP GET request | Web service backends |
ping |
ICMP ping | Network-level check |
# Global health check defaults, applied to every backend that does not define
# its own `health_check` block.
health_check:
type: "tcp" # tcp, http, or ping
target: "1.1.1.1:443" # required; a backend with no target is skipped
interval: "30s"
timeout: "5s"
healthy_threshold: 2 # consecutive successes before marking healthy
unhealthy_threshold: 3 # consecutive failures before marking unhealthy
# Per-backend override (in backend definition)
backends:
- name: "germany"
type: "wireguard"
config:
config_file: "/path/to/germany.conf"
health_check:
type: "http"
target: "api.internal:8443"
path: "/healthz"
scheme: "https" # http (default) or https; type: http only
insecure_skip_verify: true # requires scheme: https
interval: "15s"
timeout: "3s"
healthy_threshold: 2
unhealthy_threshold: 3| Field | Type | Default | Notes |
|---|---|---|---|
type |
string | tcp |
tcp, http, ping |
target |
string | — | host:port (or host for ping); required |
path |
string | /health |
type: http only |
scheme |
string | http |
http or https; rejected for non-HTTP types |
insecure_skip_verify |
bool | false |
Rejected unless scheme: https |
interval |
duration | 30s |
|
timeout |
duration | 5s |
|
healthy_threshold |
int | 1 |
<= 0 is treated as 1 |
unhealthy_threshold |
int | 1 |
<= 0 is treated as 1 |
Both the global and per-backend blocks are validated by
config.HealthCheckConfig.Validate, which rejects settings that could never take
effect (an unknown scheme, a scheme or insecure_skip_verify on a non-HTTP
check, insecure_skip_verify without scheme: https) rather than silently
ignoring them. All fields are editable from the server dashboard.
Changes to health_check require a server restart; the dashboard labels the
section accordingly.
- Faster response to real-world issues
Note
Passive health checks supplement active probing. If a backend fails multiple consecutive real requests, it is temporarily marked unhealthy even if the next active probe hasn't triggered yet.
When a rule specifies multiple backends, load balancing distributes traffic:
rules:
- name: "Streaming with failover"
match:
domains: ["*.crunchyroll.com"]
backends: # Multiple backends for load balancing
- name: "germany-1"
weight: 50
- name: "germany-2"
weight: 50
load_balancing:
algorithm: "round_robin" # round_robin, least_conn, random, ip_hash
sticky_sessions: false # Keep same client on same backendGET /api/backends/:name/health - Get backend health status
{
"name": "germany",
"status": "healthy", // healthy, unhealthy, unknown
"last_check": "2024-01-15T10:30:00Z",
"latency_ms": 45,
"consecutive_failures": 0,
"uptime_percent": 99.8
}
The server exposes Prometheus-compatible metrics at /metrics:
# Enable in config
metrics:
enabled: true
endpoint: "/metrics"
port: 7090 # Separate port for metrics (optional)# Connection metrics
proxy_connections_total{backend="germany",status="success"} 12345
proxy_connections_active{backend="germany"} 42
proxy_connections_errors_total{backend="germany",error="timeout"} 5
# Request metrics
proxy_requests_total{method="CONNECT",backend="germany"} 5000
proxy_request_duration_seconds{backend="germany",quantile="0.99"} 0.250
proxy_request_bytes_total{direction="in"} 1234567890
proxy_request_bytes_total{direction="out"} 9876543210
# Backend health
proxy_backend_healthy{backend="germany"} 1
proxy_backend_latency_seconds{backend="germany"} 0.045
# Rate limiting
proxy_rate_limit_exceeded_total{by="ip"} 100
proxy_rate_limit_exceeded_total{by="user"} 25
# System
proxy_uptime_seconds 86400
proxy_config_reload_total 3
proxy_config_reload_errors_total 0
Pre-built Grafana dashboard available at docker/grafana/dashboards/bifrost-overview.json with:
- Request rate and error rate graphs
- Backend health status
- Latency histograms
- Connection pool utilization
- Rate limiting statistics
GET /health - Simple health check (200 OK or 503)
GET /health/ready - Readiness check (config loaded, backends available)
GET /health/live - Liveness check (process running)
stateDiagram-v2
[*] --> Running
Running --> Stopping : SIGTERM / SIGINT
Stopping --> Draining : Close Sockets
Draining --> Cleanup : Drain Timeout / Finished
Cleanup --> [*] : Terminate
- Stop accepting new connections - Close listening sockets
- Drain existing connections - Allow in-flight requests to complete
- Timeout - Force close after drain timeout
- Cleanup - Close VPN tunnels, flush logs
# Configuration
shutdown:
drain_timeout: "30s" # Max time to wait for connections to drain
force_timeout: "60s" # Force shutdown after this timeReload configuration without dropping connections:
# Via CLI
bifrost-server config reload
# Via signal
kill -HUP <pid>
# Via API
POST /api/config/reloadWhat can be hot-reloaded — exactly four config sections, applied by
(*server.Server).ReloadConfig:
| Section | Applied on reload |
|---|---|
routes |
Routing rules are reloaded into the router |
rate_limit |
Request rate limits and bandwidth throttles |
access_control |
IP whitelist / blacklist |
cache |
Cache rules and presets (storage settings are ignored; enabling caching on a server that started with it disabled still needs a restart) |
What requires a restart — every other section: server (listeners, TLS,
mTLS), backends, auth, access_log, metrics, logging, web_ui, api,
health_check, auto_update, network, session, mitm, plus cache storage
changes.
GET /api/v1/config/meta returns this classification per section, and
PUT /api/v1/config returns hot_reloaded_sections and
restart_required_sections for the specific save. Both are derived from the same
table in internal/api/server/config_handlers.go, so API clients and the
dashboard never re-derive it and cannot disagree with the server.
A save that changes both kinds of section still hot-applies the hot-reloadable part; it does not defer everything to the restart.
During reload or shutdown:
- New connections go to new config/backends
- Existing connections complete with old config
- Configurable drain timeout
reload:
drain_timeout: "10s" # Drain time during hot reloadThe server supports modular authentication via a list of provider plugins under
auth.providers.
Important
The legacy auth.mode field (and the legacy type-specific top-level blocks
such as auth.native: / auth.ldap:) are deprecated and rejected at config
load time. Use auth.providers instead. Each provider has a type, an
enabled flag, a priority (lower is tried first), and a plugin-specific
config map. See Section 17 for the full plugin catalog.
| Type | Description | Use Case |
|---|---|---|
none |
No authentication required | Development, trusted networks |
native |
Server-managed users/passwords | Simple deployments |
system |
OS user authentication (Linux PAM / macOS Directory Services; not Windows, and PAM requires a non-default build tag) | Single-server with OS users |
ldap |
LDAP/Active Directory | Enterprise environments |
oauth |
OAuth 2.0 / OpenID Connect | SSO integration |
apikey, jwt, mtls, totp, hotp, kerberos, ntlm |
See Section 17 | Various |
auth:
providers:
- name: "open"
type: "none"
enabled: true
priority: 1auth:
providers:
- name: "users"
type: "native"
enabled: true
priority: 1
config:
users:
- username: "user1"
password_hash: "$2a$10$..." # bcrypt hash
- username: "user2"
password_hash: "$2a$10$..."auth:
providers:
- name: "corp-ldap"
type: "ldap"
enabled: true
priority: 1
config:
url: "ldaps://ldap.example.com:636"
bind_dn: "cn=service,dc=example,dc=com"
bind_password: "${LDAP_BIND_PASSWORD}"
base_dn: "dc=example,dc=com"
user_filter: "(sAMAccountName=%s)"auth:
providers:
- name: "sso"
type: "oauth"
enabled: true
priority: 1
config:
client_id: "${OAUTH_CLIENT_ID}"
client_secret: "${OAUTH_CLIENT_SECRET}"
auth_url: "https://auth.example.com/authorize"
token_url: "https://auth.example.com/token"
userinfo_url: "https://auth.example.com/userinfo"
scopes: ["openid", "profile", "groups"]Multiple providers may be enabled simultaneously; they are tried in priority
order until one authenticates the request.
Clients authenticate using the Proxy-Authorization header:
Proxy-Authorization: Basic base64(username:password)
For OAuth, clients can use bearer tokens:
Proxy-Authorization: Bearer <access_token>
The Web UI uses the same authentication backend. Session-based authentication is used after initial login.
# Add user
bifrost-server user add --username john --groups streaming,work
# Remove user
bifrost-server user remove --username john
# List users
bifrost-server user list
# Reset password
bifrost-server user passwd --username john
# Manage groups
bifrost-server user groups --username john --add admin
bifrost-server user groups --username john --remove streaming# client-config.yaml
client:
http_port: 7380
rules:
- name: "Claude via server"
match:
domains: ["*.anthropic.com", "*.claude.ai"]
action: "server"
- name: "Default direct"
match:
domains: ["*"]
action: "direct"Then set HTTP_PROXY=http://127.0.0.1:7380 for Claude Code.
# server-config.yaml
backends:
- name: "germany"
type: "wireguard"
wireguard:
config_file: "/etc/wireguard/germany.conf"
rules:
- name: "Crunchyroll Germany"
match:
domains: ["*.crunchyroll.com"]
backend: "germany"The project uses Astro with the Starlight documentation theme, deployed to GitHub Pages.
Location: docs/ directory (Astro/Starlight project; content under docs/src/content/docs/)
Configuration: docs/astro.config.mjs
Build: make docs-build (runs npm run build in docs/) or make docs-serve for local development
Deployment: Automatically deployed via GitHub Actions workflow (.github/workflows/docs.yml)
Live Site: https://rennerdo30.github.io/bifrost-proxy/
All diagrams in the documentation use Mermaid for rendering. Mermaid provides interactive, scalable diagrams that work well in web browsers.
Why Mermaid?
- Supported in the Astro/Starlight docs site (via the Mermaid integration) and in GitHub Markdown
- Interactive and scalable
- Text-based (version control friendly)
- Wide variety of diagram types
- Better accessibility than ASCII art
Usage:
```mermaid
graph LR
A[Node A] --> B[Node B]
B --> C[Node C]
```Supported Diagram Types:
- Flowcharts/Graphs:
graphorflowchart- For architecture, process flows - Sequence Diagrams:
sequenceDiagram- For request/response flows - Class Diagrams:
classDiagram- For code structure - State Diagrams:
stateDiagram- For state machines - Entity-Relationship:
erDiagram- For data models - Gantt Charts:
gantt- For timelines - Pie Charts:
pie- For statistics - Git Graphs:
gitGraph- For version control flows
Example - Architecture Diagram:
The architecture diagrams shown in sections 2.1 and 2.2 are rendered as Mermaid diagrams in the live documentation:
graph LR
Browser[Browser / App] -->|HTTP/SOCKS5| Client[Client<br/>local]
Client -->|HTTP/SOCKS5| Server[Server<br/>central]
Server -->|Tunnel| WireGuard[WireGuard<br/>Tunnel]
Server -->|Tunnel| OpenVPN[OpenVPN<br/>Tunnel]
Server -->|Proxy| HTTPProxy[HTTP/SOCKS5<br/>Proxy]
Best Practices:
- Use descriptive node labels
- Add styling with
styledirectives for visual clarity - Keep diagrams focused and simple
- Choose appropriate diagram types for the content
- Test locally with
make docs-servebefore committing
Note: The ASCII diagrams in previous versions of this specification have been converted to Mermaid diagrams for better rendering and interactivity across all platforms.
Automatic Deployment:
- Triggers on push to
mainbranch when files indocs/,README.md,CHANGELOG.md, orCONTRIBUTING.mdchange - Builds documentation using Node.js (Astro/Starlight,
npm run buildindocs/) - Deploys to GitHub Pages automatically
Manual Trigger:
gh workflow run "Documentation"Local Development:
# Start local server (auto-reload)
make docs-serve
# Build static site
make docs-buildThe documentation is organized into the following sections:
- Home (
index.md) - Overview and quick start - Getting Started - Installation and setup guides
- Configuration - Server and client configuration
- Overview
- Backends (WireGuard, OpenVPN, HTTP/SOCKS5 proxies)
- Authentication (None, Native, System, LDAP, OAuth)
- OpenWRT LuCI Setup Guide
- Deployment - Docker, systemd, launchd
- Operations
- CLI Reference
- Monitoring (Prometheus, Grafana)
- Security
- Troubleshooting
- API Reference - REST API documentation
- Development
- Contributing
- Changelog
The authentication system has been refactored to use a plugin architecture, allowing for modular authentication providers that can be combined and extended.
graph LR
C[Client Request] --> PM[Plugin Manager]
subgraph "Auth Plugins"
PM --> P1[None]
PM --> P2[Native]
PM --> P3[LDAP]
PM --> P4[OAuth]
PM --> P5[...]
end
P1 & P2 & P3 & P4 & P5 --> Session[Session Store]
type Plugin interface {
// Name returns the unique identifier for this plugin
Name() string
// Init initializes the plugin with configuration
Init(config map[string]interface{}) error
// Authenticate validates credentials and returns user info
Authenticate(ctx context.Context, creds Credentials) (*User, error)
// Close cleans up resources
Close() error
}| Plugin | Description | Use Case |
|---|---|---|
none |
No authentication | Development, trusted networks |
native |
Server-managed users/passwords | Simple deployments |
system |
OS user authentication (PAM/Directory Services) | Single-server with OS users |
ldap |
LDAP/Active Directory | Enterprise environments |
oauth |
OAuth 2.0 / OpenID Connect | SSO integration |
apikey |
API key in header | Service-to-service auth |
jwt |
JWT token validation with JWKS | Token-based auth |
totp |
Time-based OTP | Google Authenticator |
hotp |
Counter-based OTP | YubiKey |
mtls |
Client certificate auth | Smart cards, certificates |
kerberos |
Kerberos/SPNEGO | Enterprise SSO |
ntlm |
Non-functional — fails closed | Rejects every login; use Kerberos instead |
Warning
The ntlm plugin cannot verify NTLM responses (Bifrost has no credential
source to recompute the Type 3 response against), so it rejects every
authentication attempt to avoid an auth bypass. It is retained only so a
misconfiguration does not silently fall through to another provider. Use
kerberos (SPNEGO) for Windows domain SSO.
negotiate is HTTP middleware, not a provider. SPNEGO/Negotiate browser
SSO is enabled via the auth.negotiate block (which references a kerberos
provider by name), not via a type: negotiate entry in auth.providers[].
The MFA wrapper allows combining a primary authentication method with an OTP provider.
Note
Like all auth configuration, the MFA wrapper is declared as a provider under
auth.providers with a config map (the legacy top-level auth.mode /
auth.mfa_wrapper form is rejected). The exact config keys are
plugin-specific — consult internal/auth/ and the Starlight docs for the
authoritative shape. Conceptually it nests a primary provider and a secondary
OTP provider (e.g. native + totp).
Sessions can be stored in memory or Redis:
session:
store: redis # or "memory"
redis:
address: "localhost:6379"
password: ""
db: 0
ttl: "24h"
cookie_name: "bifrost_session"The client supports a TUN-based VPN mode that captures all system traffic and routes it through the proxy.
graph LR
TUN[TUN Device<br/>bifrost0] --> Rules[Split Rules]
Rules --> Router[Router]
Router -->|Route| Direct[Direct Connection]
Router -->|Route| Server[Server Connection]
Router -->|Route| Block[Block / Drop]
vpn:
enabled: true
mode: tun
interface_name: bifrost0
mtu: 1420
# DNS interception
dns:
enabled: true
servers:
- "1.1.1.1"
- "8.8.8.8"
cache_ttl: "5m"
# Split tunneling rules
split:
mode: exclude # "include" or "exclude"
# App-based rules (by process name or path)
apps:
- name: "Slack"
path: "/Applications/Slack.app"
- name: "Teams"
# Domain-based rules
domains:
- "*.internal.company.com"
- "localhost"
- "*.local"
# IP/CIDR-based rules
ips:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "172.16.0.0/12"- Include Mode: Only traffic matching rules goes through VPN
- Exclude Mode: All traffic except matching rules goes through VPN
| Platform | TUN Support | App Rules | Notes |
|---|---|---|---|
| Linux | ✅ | Process matching | Requires CAP_NET_ADMIN |
| macOS | ✅ | Bundle ID matching | Uses utun interface |
| Windows | ✅ | Process path matching | Requires wintun driver |
The desktop client is a native Wails quick-access wrapper around the same embedded Go client used by bifrost-client. It is deliberately smaller than the web dashboard.
- Start and stop the embedded local proxy client
- Report local lifecycle and upstream reachability separately
- Show active connections, cumulative bytes sent/received, local proxy addresses, uptime, VPN state, and the last error
- Add, edit, delete, select, and mark named upstream servers as default
- Edit the upstream address/protocol and local HTTP/SOCKS5 ports; restart the client to apply listener changes
- Enable/disable an already-configured VPN manager
- Persist Auto-connect and Start-minimized GUI preferences
- Provide one process-lifetime tray across client Start/Stop cycles
The desktop window does not implement split-tunnel rule editing, traffic-log viewing, recent-connection tables, bandwidth graphs, or update controls. Those remain web-dashboard/config-file surfaces.
graph LR
FE[Wails React quick-access UI] <--> APP[desktop.App bindings]
APP <--> CORE[Embedded Bifrost client]
CORE --> HTTP[Local HTTP proxy]
CORE --> SOCKS[Local SOCKS5 proxy]
CORE --> API[Client API and web dashboard]
CORE --> VPN[Optional VPN manager]
CORE --> TRAY[Process-wide system tray]
CORE --> UPSTREAM[Configured Bifrost server]
TRAY --> FE
The tray's Connect/Disconnect action controls operating-system proxy settings; it is distinct from both the Wails window's local-client lifecycle button and its VPN-mode toggle. Quick Access restores the Wails window; Open Dashboard opens the client web UI.
The app loads the ordinary config.ClientConfig YAML, forces the local API on for the embedded dashboard, and saves server/listener changes back to that file. GUI-only preferences live in quick-preferences.json below the user config directory. Auto-connect defaults to true. Start-minimized hides the initial Wails window, which can be restored from the tray.
Wails GUI binaries are built on native Linux, Windows, and macOS GitHub runners. Tagged releases attach:
bifrost-desktop-linux-amd64.tar.gzbifrost-desktop-windows-amd64.exebifrost-desktop-darwin-universal.zip
go install github.com/wailsapp/wails/v2/cmd/wails@v2.15.0
make desktop-buildUbuntu 24.04 builds require GTK3 and WebKitGTK 4.1 development packages and the webkit2_41 build tag. See the desktop-client documentation for exact commands and runtime requirements.
A cross-platform mobile client built with React Native and Expo.
- Home Screen: VPN connection status and quick toggle
- Servers Screen: Server list with latency indicators
- Stats Screen: Real-time traffic statistics
- Settings Screen: Configuration management
graph TD
subgraph "Application"
Screens[React Native Screens] --> Hooks[React Query Hooks]
Hooks --> API[API Proxy Service]
end
API -->|REST| Bifrost[Bifrost Client API]
| Screen | Description |
|---|---|
| Home | Connection status, VPN toggle, traffic summary |
| Servers | Server list with status, latency, selection |
| Stats | Detailed statistics, connection info, network details |
| Settings | Auto-connect, kill switch, server address, notifications |
# Install dependencies
cd mobile
npm install
# Start development
npx expo start
# Build for iOS
npx expo build:ios
# Build for Android
npx expo build:android
# Using EAS Build (recommended)
eas build --platform ios
eas build --platform androidThe mobile client uses React Query for data fetching with automatic:
- Background refetching (configurable intervals)
- Optimistic updates for mutations
- Error handling and retry logic
- Cache invalidation on mutations
Bifrost includes a built-in update mechanism that checks GitHub for new releases.
- stable: Production-ready releases.
- prerelease: Beta and release candidates.
- nightly: Automated builds from the master branch (if available).
Users can manually check for and install updates using the update command.
bifrost-client update check
bifrost-client update install --channel stableThe application can be configured to check for updates in the background at regular intervals.
auto_update:
enabled: true
check_interval: "24h"
channel: "stable"Both daemons own the checker's lifecycle: Server.Start / Client.Start create
the updater when enabled is true and stop the background checker during
graceful shutdown. The first check runs one minute after startup. On the server
check_interval is clamped to a minimum of one hour to stay within the
unauthenticated GitHub Releases API rate limit; a non-positive value falls back
to 24 hours. Available updates are reported through the Notifier interface —
an INFO log line on the server, and a log line plus a desktop notification on
the client. The server never installs an update on its own; installation stays a
deliberate bifrost-server update install action.
Bifrost provides native service management for Windows (SCM), macOS (launchd), and Linux (systemd).
install: Registers the binary as a system service with specified configuration.start: Starts an installed service through systemd, launchd, or Windows SCM.stop: Stops an installed service without removing it.uninstall: Unregisters and removes the service.status: Displays the current service status.
Every control command returns platform-tool failures and their diagnostic output; missing service tools and permission errors never report success.
- Windows: Registers as a Windows Service using the SCM. Supports START, STOP, and SHUTDOWN events.
- macOS: Generates and installs a
.plistfile in~/Library/LaunchAgents. - Linux: Generates and installs a
.serviceunit file in/etc/systemd/system.
The Bifrost client can automatically configure the operating system's proxy settings.
system_proxy:
enabled: trueOn Windows, Bifrost modifies the registry keys under Software\Microsoft\Windows\CurrentVersion\Internet Settings and notifies the system using InternetSetOption from wininet.dll.
When updating configuration via the REST API or CLI, Bifrost uses an AST-based approach (using yaml.v3) to ensure that:
- User comments are preserved.
- Formatting and indentation are maintained.
- Only the specific requested fields are modified.