Skip to content

Configuration ​

rtpbridge is configured via CLI arguments and/or a TOML config file. CLI arguments override config file values.

CLI Arguments ​

bash
rtpbridge [OPTIONS]
ArgumentDefaultDescription
-l, --listen <ADDRS>127.0.0.1:9100WebSocket/HTTP control plane listen address(es), comma-separated
-m, --media-ip <IP>127.0.0.1IP address for all media sockets
-c, --config <PATH>—Path to TOML configuration file
--log-level <LEVEL>infoLog level: trace, debug, info, warn, error

TOML Config File ​

toml
# WebSocket/HTTP control plane (comma-separated addresses accepted)
listen = "127.0.0.1:9100"

# Optional HMAC control-plane authorization. The value is read from a mounted
# file, never written inline in this config. When configured, control WebSocket
# upgrades and sensitive HTTP routes require the signed Authorization header
# documented below. `/health`, `/metrics`, and single-use `/audio/<token>`
# connections intentionally remain separate capabilities.
auth_hmac_secret_file = "/etc/rtpbridge/auth/control-hmac"
auth_hmac_max_age_secs = 60

# Media plane IP(s) — used for RTP sockets and SDP/ICE candidates.
# A single address, or a comma-separated IPv4+IPv6 pair for dual-stack.
media_ip = "203.0.113.5"
# media_ip = "203.0.113.5, 2001:db8::5"   # dual-stack

# UDP port range for plain RTP endpoints [start, end]
rtp_port_range = [30000, 39999]

# Session disconnect timeout (seconds)
# When a WS connection drops, the session stays alive for this long
disconnect_timeout_secs = 30

# Maximum shutdown drain wait (seconds)
# On SIGINT/SIGTERM, wait this long for sessions to finish
shutdown_max_wait_secs = 300

# File cache directory for URL downloads
cache_dir = "/tmp/rtpbridge-cache"

# How often to clean up expired cache entries (seconds)
cache_cleanup_interval_secs = 300

# Allowed base directory for local file playback
# If unset, local file playback is disabled; URLs need an allowed origin
# media_dir = "/var/lib/rtpbridge/media"

# Base directory for PCAP recordings
# Recorded files are also served over HTTP from this path (GET /recordings/<path>)
recording_dir = "/var/lib/rtpbridge/recordings"

# Resource limits
max_sessions = 10000
max_endpoints_per_session = 20

# Maximum concurrent PCAP recordings per session
max_recordings_per_session = 100

# Seconds to observe recording flush before reporting a slow writer
recording_flush_timeout_secs = 10

# Maximum concurrent HTTP downloads for URL-based file playback (default: 16)
max_concurrent_downloads = 16

# Connection limits
max_connections = 1000
ws_ping_interval_secs = 30
event_channel_size = 256
critical_event_channel_size = 64

# WebSocket and SDP size limits
ws_max_message_size_kb = 256             # Max WebSocket message size
max_sdp_size_kb = 64                     # Max SDP offer/answer size

# Session idle timeout (0 = disabled)
session_idle_timeout_secs = 0

# Empty session timeout (0 = disabled)
# Sessions with zero endpoints for this duration are auto-destroyed
# empty_session_timeout_secs = 0

# Media timeout event threshold (seconds)
media_timeout_secs = 5

# Recording channel buffer size (packets)
recording_channel_size = 1000

# Transcode and download limits
transcode_cache_size = 64
max_file_download_bytes = 104857600       # 100 MB
max_recording_download_bytes = 536870912  # 512 MB

# Log level
log_level = "info"

# Optional: enable TLS for every HTTP/WebSocket connection on `listen`. When
# configured, the same port serves HTTPS/WSS rather than plaintext HTTP/WS.
# Both files must be PEM encoded. Omit the whole table to keep the legacy
# plaintext listener for local development. This table is last because TOML
# keys that follow it would otherwise belong to the `tls` table.
[tls]
cert_path = "/etc/rtpbridge/tls/tls.crt"
key_path = "/etc/rtpbridge/tls/tls.key"

Split Interface Binding ​

rtpbridge supports binding the control plane and media plane to different network interfaces:

  • listen — The WebSocket control plane. Bind to your management network (e.g., 10.0.1.5:9100).
  • media_ip — All RTP/WebRTC UDP sockets. Bind to your media network (e.g., 10.0.2.5). Accepts a single address or a comma-separated list — at most one IPv4 and one IPv6 — to dual-bind both families on one instance.

The media_ip is used directly in:

  • SDP c= lines (connection address)
  • ICE host candidates for WebRTC
  • Plain RTP socket binding

No STUN/TURN discovery is performed — the configured IP is assumed to be directly reachable.

Dual-stack (IPv4 + IPv6) ​

Set media_ip to one IPv4 and one IPv6 address to serve both families from a single instance:

toml
media_ip = "203.0.113.5, 2001:db8::5"

Behavior:

  • Plain RTP/SRTP answers each offer with the family matching the remote SDP's c= line, allocating from that family's port pool. An offer for a family you did not configure is rejected (ENDPOINT_ERROR) rather than answered with an unreachable address. Re-negotiations (update_remote_sdp) that flip a bound endpoint's family are likewise rejected — sockets are not migrated across families.
  • WebRTC binds one UDP socket per family and offers an ICE host candidate for each; ICE nominates the working pair. (Note: the ICE library's default local preference favors IPv6, so a dual-stack browser typically connects over IPv6.)
  • Offers rtpbridge originates for plain RTP advertise a single c= line and prefer IPv4 when both families are configured.

Validation rejects unspecified (0.0.0.0 / ::) and multicast media addresses; loopback and link-local addresses warn (IPv6 link-local cannot carry a scope id in SDP).

Limitation: PCAP recordings always synthesize IPv4 framing regardless of the real media family — recorded packets do not carry real IPv6 headers.

HTTP REST API ​

In addition to the WebSocket control protocol, rtpbridge serves HTTP endpoints on the same listen address:

EndpointMethodDescription
/healthGETHealth check — returns {"status":"ok"} with 200 OK
/metricsGETPrometheus-format metrics (OpenMetrics text exposition)
/sessionsGETList all active sessions (JSON)
/sessions/{id}GETGet session details by UUID (JSON)
/recordingsGETList .pcap recording files. Supports query params: startsWith, skip, limit
/recordings/{path}GETDownload a specific PCAP recording file
/recordings/{path}DELETEDelete a specific PCAP recording file

All HTTP responses include Connection: close. Recording file paths are validated against the configured recording_dir to prevent path traversal.

TLS and HMAC authorization ​

[tls] is all-or-nothing: when it is present, every address in listen serves TLS on the same configured port. The control WebSocket becomes wss:// and the HTTP endpoints become https://; rtpbridge does not multiplex plaintext and TLS on one port. The server authenticates its certificate but does not request client certificates.

Set auth_hmac_secret_file to enable application authorization. It must point to a readable file containing at least 32 bytes of shared key material. The secret is intentionally read from a file so it can be mounted from a secret manager rather than committed to TOML. A protected request must include:

text
Authorization: HMAC-SHA256 <unix-seconds>:<base64url-no-padding-signature>

The signature is HMAC-SHA256(secret, "rtpbridge-auth-v1\\n<unix-seconds>\\n<METHOD>\\n<request-target>"). request-target includes the path and query string exactly as sent. The server rejects a missing, malformed, expired, future-dated, or invalid signature with 401; auth_hmac_max_age_secs limits replay time (default 60 seconds).

When HMAC authorization is enabled it protects control WebSocket upgrades and the session/recording HTTP surfaces. /health and /metrics remain unauthenticated for orchestration and Prometheus. The /audio/<connect_token> WebSocket remains authorized by rtpbridge's existing server-minted, single-use opaque token; do not give an AI/media consumer the HMAC signing key merely to stream PCM.

Without HMAC, privileged HTTP and control WebSocket requests carrying Origin are rejected with 403. Unauthenticated loopback clients must also use localhost or a loopback IP in Host, preventing DNS rebinding against a development listener. Browser applications should use an authenticated backend for control and the single-use token for audio. A local proxy preserving a public Host must sign its privileged upstream requests with HMAC.

Security ​

Path Traversal Protection ​

Both recording_dir and media_dir enforce strict path validation:

  • Paths containing .. components are rejected outright
  • Symlinks in parent directories are resolved (canonicalized) before validation
  • The resolved path must reside within the configured base directory
  • This eliminates TOCTOU races — the resolved path is used directly for file creation

The HTTP recording API (GET /recordings/{path}, DELETE /recordings/{path}) applies the same validation, preventing path traversal via HTTP requests.

Response Status Codes ​

EndpointStatusCondition
GET /health200 OKAlways
GET /metrics200 OKMetrics encoded successfully
GET /metrics500 Internal Server ErrorMetrics encoding failed
GET /sessions200 OKAlways (returns [] if none)
GET /sessions/{id}200 OKSession found
GET /sessions/{id}400 Bad RequestInvalid UUID
GET /sessions/{id}404 Not FoundNo session with that ID
GET /recordings200 OKDirectory listed successfully
GET /recordings500 Internal Server ErrorRecording directory not readable
GET /recordings/{path}200 OKFile returned (application/vnd.tcpdump.pcap)
GET /recordings/{path}403 ForbiddenPath traversal attempt detected
GET /recordings/{path}404 Not FoundFile does not exist
GET /recordings/{path}413 Payload Too LargeRecording exceeds max_recording_download_bytes
DELETE /recordings/{path}200 OKFile deleted ({"deleted":true})
DELETE /recordings/{path}404 Not FoundFile does not exist
DELETE /recordings/{path}500 Internal Server ErrorDeletion failed
Any405 Method Not AllowedUnsupported HTTP method for route

Response Schemas ​

GET /sessions/ ​

json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "state": "active",
  "created_at": "2024-01-15T10:30:00Z",
  "endpoints": [
    {
      "endpoint_id": "...",
      "endpoint_type": "rtp",
      "state": "connected",
      "direction": "sendrecv",
      "codec": "PCMU",
      "local_rtp_addr": "203.0.113.5:30000",
      "local_rtcp_addr": "203.0.113.5:30001",
      "remote_rtp_addr": "198.51.100.10:40000",
      "remote_rtcp_addr": "198.51.100.10:40001"
    }
  ],
  "recordings": [
    {
      "recording_id": "...",
      "file_path": "/var/lib/rtpbridge/recordings/call-1.pcap",
      "endpoint_id": null,
      "state": "active"
    }
  ],
  "vad_active": ["endpoint-id-1"],
  "fax_detect_active": ["endpoint-id-1"]
}
FieldTypeDescription
session_idstring (UUID)The session identifier
statestring"active" or "orphaned"
created_atstring (RFC 3339)When the session was created
endpointsarrayList of endpoints in the session
recordingsarrayActive recordings
vad_activearray of stringsEndpoint IDs with active VAD
fax_detect_activearray of stringsEndpoint IDs with active fax tone detection

Recording Pagination ​

The GET /recordings endpoint supports pagination via query parameters:

ParameterDefaultDescription
startsWith—Filter recordings whose relative path starts with this prefix (URL-decoded)
skip0Number of results to skip
limit100Maximum results to return (capped at 1000)

Response body:

json
{
  "recordings": ["call-123.pcap", "call-456.pcap"],
  "total": 42,
  "skip": 0,
  "limit": 100
}

Complete Configuration Reference ​

All configuration options with their types, defaults, and descriptions. All changes require a service restart.

FieldTypeDefaultDescription
listenip:port[,ip:port...]127.0.0.1:9100WebSocket/HTTP control plane listen address(es), comma-separated
tls.cert_pathpath—Optional PEM server certificate. With tls.key_path, changes every control listener to HTTPS/WSS.
tls.key_pathpath—Optional PEM private key; must be supplied with tls.cert_path.
auth_hmac_secret_filepath—Optional mounted HMAC key file (minimum 32 bytes) for control-plane authorization.
auth_hmac_max_age_secsu6460Accepted signature age; must be 1–300 seconds.
media_ipip[,ip]127.0.0.1IP(s) for RTP/WebRTC UDP sockets; appears in SDP and ICE candidates. Comma-separated for dual-stack (≤1 IPv4, ≤1 IPv6)
rtp_port_range[u16, u16][30000, 39999]UDP port range for plain RTP endpoints (must start even, >= 1024)
disconnect_timeout_secsu6430Seconds to keep orphaned sessions alive after WebSocket disconnect
shutdown_max_wait_secsu64300Maximum wait for session drain on graceful shutdown
media_dirpath?(none)Base directory for local file playback; unset disables local files
recording_dirpath/var/lib/rtpbridge/recordingsPCAP recording output and HTTP serving directory
cache_dirpath/tmp/rtpbridge-cacheFile cache directory for URL downloads
cache_cleanup_interval_secsu64300Interval for cache cleanup of expired entries
max_concurrent_downloadsusize16Maximum concurrent HTTP downloads for URL file playback (1..256)
max_sessionsusize10000Maximum concurrent sessions (1..65536)
max_endpoints_per_sessionusize20Maximum endpoints per session (1..128)
max_recordings_per_sessionusize100Maximum concurrent recordings per session
recording_flush_timeout_secsu6410Seconds to wait for recording tasks to flush on stop
ws_max_message_size_kbusize256Maximum WebSocket message/frame size in KB
max_sdp_size_kbusize64Maximum SDP size in KB; rejects oversized offers/answers
session_idle_timeout_secsu640Auto-destroy sessions with no activity for this duration (0 = disabled)
empty_session_timeout_secsu640Auto-destroy sessions with zero endpoints for this duration (0 = disabled)
max_connectionsusize1000Maximum concurrent WebSocket connections (0 = unlimited)
ws_ping_interval_secsu6430WebSocket ping interval for keepalive and dead connection detection
event_channel_sizeusize256Buffer size for normal event channel per connection
critical_event_channel_sizeusize64Buffer size for priority event channel per connection
transcode_cache_sizeusize64Maximum entries in the transcode pipeline LRU cache per session
max_file_download_bytesu64104857600Maximum file download size (100 MB) for URL playback
max_recording_download_bytesu64536870912Maximum recording file size (512 MB) for HTTP GET serving
recording_channel_sizeusize1000Buffer size (packets) for the channel between session task and recording writer task
media_timeout_secsu645Seconds without RTP packets before firing endpoint.media_timeout event
log_levelstringinfoLog level: trace, debug, info, warn, error

Validation Rules ​

All values are validated at startup. Invalid configurations cause an immediate exit with a descriptive error.

Numeric Lower Bounds ​

The following fields must be greater than zero:

disconnect_timeout_secs, shutdown_max_wait_secs, cache_cleanup_interval_secs, max_concurrent_downloads, max_recordings_per_session, recording_flush_timeout_secs, ws_max_message_size_kb, max_sdp_size_kb, ws_ping_interval_secs, event_channel_size, critical_event_channel_size, recording_channel_size, transcode_cache_size, media_timeout_secs

max_sessions and max_endpoints_per_session require finite, nonzero limits. transcode_cache_size must be at least max_endpoints_per_session, so every active single-source destination can retain its encoder without cache churn. max_connections retains its legacy 0 meaning of unlimited connections.

Numeric Upper Bounds ​

FieldMaximumNotes
ws_max_message_size_kb512000512 MB
max_sdp_size_kb1000010 MB
media_timeout_secs3005 minutes

Port Range Rules ​

  • Start must be ≤ end
  • Both ports must be ≥ 1024 (privileged ports are not allowed)
  • Start must be even (RTP uses even/odd port pairs per RFC 3550)
  • End must be ≤ 65534 (reserving room for the RTCP port in each pair)
  • Range must span at least 2 ports, i.e. one even/odd pair (e.g., [30000, 30001])

Path Validation ​

  • media_dir (if set): must exist and be a directory
  • recording_dir: parent directory must exist (unless using the default /var/lib/rtpbridge/recordings); must be writable if it already exists
  • cache_dir: parent directory must exist (unless using the default /tmp/rtpbridge-cache); must be writable if it already exists

Control listeners default to 127.0.0.1:9100. Every non-loopback address, including a wildcard listener, requires both TLS and auth_hmac_secret_file. allow_plaintext_control = true explicitly permits the protected upstream of a trusted TLS proxy; it does not waive HMAC. allow_unauthenticated_control = true is a separate development exception. Both default to false. Local loopback clients remain trusted, and one HMAC key grants administrative access across sessions.

rtp_source_networks controls which IPs can establish a symmetric RTP or RTCP tuple before it is latched. It defaults to ["*"], so a valid initial media packet may arrive from anywhere — required for direct clients behind arbitrary NATs. Once a packet is accepted, RTPbridge pins the exact IP and port and rejects a different tuple until an accepted SDP renegotiation or direction reset. Set [] for SDP-IP-only admission, or use narrow IPv4/IPv6 CIDRs for trusted SBCs or media gateways. The SDP peer IP is always accepted during initial latching. Separate RTCP learns and pins its own tuple; with rtcp-mux, RTP and RTCP share one tuple, while a=rtcp is honored for non-mux calls. An open first-packet policy does not authenticate plain RTP: use SRTP, network filtering, or narrow CIDRs where an on-path or reachable host must not be able to win the initial latch.

Playback destination policy and resource limits ​

Remote playback is disabled until file_download_origins contains the exact HTTP(S) origin. Origins include scheme, host and port, with no path, userinfo, query or fragment. Every redirect is checked again. Public destination IPs are permitted for approved origins; private, loopback and special-use addresses require an explicit network allowance. DNS results are pinned to the connection and environment proxies are disabled. HTTPS downgrade is rejected. A cross-origin redirect removes all caller-supplied headers.

System DNS work has a separate four-job process limit. A cancelled request retains its DNS slot until the actual system resolver call exits; saturation returns DNS_BUSY through the download error. This prevents repeated cancellation from accumulating blocking resolver jobs.

toml
file_download_origins = ["https://media.example.com", "http://10.20.0.5:8080"]
file_download_networks = ["10.20.0.5/32"]
max_concurrent_downloads = 16
max_pending_downloads = 64
max_download_owners = 256
max_file_download_bytes = 104857600
max_cache_entries = 1000
max_cache_bytes = 1073741824

Download owner deadlines include admission queue time. A shared transfer has a maximum lifetime of 60 seconds. Removing the last owner cancels the transfer; another owner's shorter deadline cannot cancel a surviving owner's request. Overload returns DOWNLOAD_BUSY or CACHE_FULL without creating an unbounded waiting task.

Use a dedicated cache directory for each process. The server locks .rtpbridge.lock and recovers only files beginning rtpbridge-cache-. Files from older hash-name cache implementations are not automatically deleted; clear that old cache during a stopped-server migration if its disk space must be reclaimed. Header variants have separate cache identities, and playback retains the exact file lease until its decoder finishes.

The cache budget includes active download reservations, temporary files and files awaiting deletion. max_file_download_bytes must be nonzero and fit within max_cache_bytes. The new owner, pending and entry counts must be between 1 and 65,536. All pinned entries can cause admission to fail even when their TTL has expired.

See performance and capacity for fixed process bounds on storage workers, playback streams, recording writers and WebSocket buffers. Control input has a 1 MiB total byte budget across the active request and up to 16 queued requests, in addition to the configured WebSocket wire limit.