Quikcast Help

Quikcast configuration guide

Checked against the working-tree implementation on 2026-10-08. This guide describes the current configuration schema, including native FLAC limits, continuous fallback mounts and listener access control added after the original feature-freeze document. Historical review documents describe the code at their own review dates; the current Rust configuration structs and validators are the authority for accepted settings.

Quikcast reads one UTF-8 TOML file at startup. It serves externally encoded continuous audio, pulls configured continuous relays, and independently serves externally produced MPEG-TS HLS assets. The daemon does not run FFmpeg, encode, decode, transcode, remux, generate HLS from a continuous mount, or run an AutoDJ.

Contents

  1. Start the server and choose a configuration

  2. Complete local example: BUTT, relay, management and HLS

  3. TOML rules, paths and defaults

  4. Top-level settings

  5. Credentials and secret files

  6. Continuous mounts

  7. Fallback mounts: configuration and behavior

  8. Listener access control

  9. Continuous resource limits

  10. Native FLAC limits

  11. Continuous relays

  12. HLS configuration and producer responsibilities

  13. Management, health and metrics

  14. TLS, reverse proxies and forwarded addresses

  15. Drain, shutdown, restart and rotation

  16. Startup rejection and troubleshooting

  17. Fixed limits and settings that do not exist

Start the server and choose a configuration

From the repository root:

cargo run --locked -- --config secrets/dev/quikcast.toml

Or run an already built binary:

./target/debug/quikcast --config secrets/dev/quikcast.toml

The executable also supports configuration selection through the environment:

QUIKCAST_CONFIG=secrets/dev/quikcast.toml ./target/debug/quikcast

An explicit --config PATH takes precedence over QUIKCAST_CONFIG. There is no implicit search for quikcast.toml: supply the flag or environment variable. The configuration filename is arbitrary.

The only daemon command-line option is --config PATH. Extra arguments reject startup. There is no --check-config, --mount, --port, reload command, or built-in operator CLI. To check acceptance, start an isolated instance using unused bindings and disposable credentials; successful startup and readiness establish that configuration acceptance completed. A port-zero binding is useful for automated checks, not a stable player URL.

To override only logging:

QUIKCAST_LOG=quikcast=debug ./target/debug/quikcast --config secrets/dev/quikcast.toml

QUIKCAST_LOG overrides log_filter when it is valid Unicode. No other TOML field has an environment override. RUST_LOG is not read by this executable. Invalid log-filter syntax rejects startup.

Configuration and credentials are loaded before serving. Editing a file while the server runs does not apply a change; restart is required. Stop an existing instance before starting another on the same ports.

Complete local example: BUTT, relay, management and HLS

This example follows the tested arrangement: BUTT publishes to /beef, while /radio.mp3 is reserved for an upstream relay. HLS has its own producer API and is not a third continuous mount.

Save the following as secrets/local-example/quikcast.toml. The numbers are illustrative functional limits, not a tested production capacity rating.

# Root settings must appear before the named tables below. listen = "127.0.0.1:8000" admin_listen = "127.0.0.1:9090" log_filter = "quikcast=info" trusted_proxies = [] [limits] ring_bytes = 4194304 ring_entries = 4096 join_bytes = 65536 flac_initialization_bytes = 65536 flac_frame_bytes = 1048576 max_connections = 256 max_handshakes = 32 max_listeners = 128 listeners_per_mount = 64 retained_audio_budget = 67108864 relay_allocation_budget = 2097152 header_timeout_ms = 5000 source_timeout_ms = 10000 write_timeout_ms = 10000 shutdown_timeout_ms = 10000 [management] token_file = "management-token" [hls] username = "hls" secret_file = "hls-ingest" max_streams = 16 renditions_per_stream = 8 max_renditions = 64 max_segment_bytes = 2097152 max_segments = 1024 max_uploads = 4 rendition_budget = 134217728 allocation_budget = 536870912 # Uncomment for an actual browser player on this exact origin: # cors_origin = "https://player.example.com" [[mounts]] path = "/beef" username = "source" secret_file = "beef-source" listener_limit = 32 [[mounts]] path = "/radio.mp3" username = "source" secret_file = "radio-local" listener_limit = 32 [[relays]] mount = "/radio.mp3" url = "http://152.53.111.31:8000/lofi" enabled = true connect_timeout_ms = 10000 header_timeout_ms = 5000 audio_timeout_ms = 10000 initial_backoff_ms = 1000 max_backoff_ms = 60000 stable_connection_ms = 30000 jitter_percent = 20

Generate the four separate credentials once, before first startup:

mkdir -p secrets/local-example chmod 700 secrets/local-example umask 077 openssl rand -hex 32 > secrets/local-example/beef-source openssl rand -hex 32 > secrets/local-example/radio-local openssl rand -hex 32 > secrets/local-example/hls-ingest openssl rand -hex 32 > secrets/local-example/management-token

These commands overwrite existing files if repeated. Generate a new directory for a new example; do not accidentally rotate a running setup's credentials. Generated files contain 64 hexadecimal characters and a trailing LF, which the loader strips. Keep real secrets out of version control, screenshots and shared reports.

Run:

./target/debug/quikcast --config secrets/local-example/quikcast.toml

For BUTT, use Icecast, address 127.0.0.1, port 8000, mount /beef, username source, and the contents of beef-source as the password. Select an audio format the encoder and Quikcast both support. For the existing MP3 validation, select MP3.

Listener URLs are http://127.0.0.1:8000/beef and http://127.0.0.1:8000/radio.mp3. Each works only while its source generation has playable media. Relay startup is asynchronous: global readiness does not mean every upstream has connected.

HLS playback URLs appear only after an external producer registers streams/renditions, uploads segments and publishes their descriptions. The URL shape is http://127.0.0.1:8000/hls/STREAM/master.m3u8; do not create a [[mounts]] entry for it.

TOML rules, paths and defaults

  • [[mounts]] and [[relays]] are arrays of tables. Repeat the header for every mount/relay.

  • [limits], [management], [hls] and [access] are single tables. [mounts.access] belongs to its preceding [[mounts]] entry. Do not repeat them as arrays.

  • Root keys must be outside those tables. Placing listen after [limits] makes it a limits key and startup rejects it.

  • Integers are plain byte counts, counts or milliseconds as documented. Strings such as "4MiB" and "10s" are not accepted. TOML integer separators such as 4_194_304 are valid; 1 MiB = 1,048,576 bytes.

  • Unknown fields are rejected at every configuration level. Misspellings do not silently fall back to defaults. Duplicate TOML keys are errors.

  • The file is limited to 131,072 bytes (128 KiB) and must be UTF-8.

  • Relative secret_file, token_file and geoip_database paths resolve beside the configuration file, not beside the executable and not relative to the shell's working directory. Absolute paths remain absolute. ~, $HOME, and ${VARIABLE} are not expanded inside TOML strings.

  • Omitted root/limits fields use their defaults. Omitted mounts/relays become empty lists; omitted trusted_proxies becomes an empty list. At least one continuous mount or an enabled [hls] section is necessary.

  • Omitting [hls] disables HLS. Including it requires valid HLS credentials, even if no stream has yet been published.

  • Omitting [access] and mount access tables leaves playback unrestricted by listener ACLs. Nonempty mount/global country rules require a shared [access].geoip_database.

  • Omitting [management] leaves native /api access fail-closed. Health/metrics on the private binding and the independently authenticated HLS producer API have their own behavior; the admin listener is still created.

Top-level settings

listen

Default: "127.0.0.1:8000". Numeric IP address and port for public continuous source/listener traffic and HLS delivery. It is a socket address, not a hostname or URL.

Examples: "127.0.0.1:8000" for local/proxy-only access, "0.0.0.0:8000" for all IPv4 interfaces, and "[::1]:8000" for IPv6 loopback. Do not assume an IPv6 wildcard also accepts IPv4 identically on every OS. Native inbound TLS is not configured here; these listeners speak HTTP/plain TCP.

Port 0 asks the OS to choose an ephemeral port. Startup logs show the actual binding. There is no enforced fixed lower port; OS privileges and conflicts still determine whether binding succeeds.

admin_listen

Default: "127.0.0.1:9090". Numeric loopback socket address for native management, private health/metrics and HLS producer operations. "[::1]:9090" is valid. A non-loopback address such as "0.0.0.0:9090" is rejected.

Public and admin addresses cannot be exactly identical when their port is nonzero. Overlapping wildcard/specific bindings can still collide at the OS even when their text differs. Keep the admin binding behind a trusted private access path; a public reverse proxy must not forward arbitrary requests into it.

log_filter

Default: "quikcast=info". A tracing EnvFilter expression. Examples: "quikcast=debug" or "quikcast=info,hyper=warn". QUIKCAST_LOG overrides it. The implementation does not offer file rotation or an in-process log database.

On Unix, stdout must be a terminal, pipe, socket or character device. Directly redirecting daemon stdout to a regular file is rejected because storage writes could block serving:

# Foreground terminal: supported. ./target/debug/quikcast --config secrets/local-example/quikcast.toml # Draining collector: supported; manage both processes in a real service setup. ./target/debug/quikcast --config secrets/local-example/quikcast.toml | tee quikcast.log # Direct regular-file stdout: rejected by the logger. # ./target/debug/quikcast --config CONFIG > quikcast.log

The CLI logger currently requires Unix. Records are bounded to 4,096 bytes and writes are nonblocking; truncated/failed/partial writes count as dropped records. Keep the collector draining. Logging is operational diagnostics, not a durable audit guarantee. When using a pipeline, manage the daemon PID/signals and collector separately rather than assuming the last pipeline process is Quikcast.

trusted_proxies

Default: []. At most 64 numeric IP addresses or CIDRs. IPv4 prefix range is 0–32; IPv6 is 0–128. A bare address means /32 or /128. Hostnames and ports are not accepted.

trusted_proxies = ["127.0.0.1/32", "::1/128", "10.10.0.5/32"]

Only trust actual controlled peers. This changes inspected client identity from X-Forwarded-For; it does not grant credentials or bypass limits. Detailed chain behavior is in TLS, reverse proxies and forwarded addresses.

Credentials and secret files

There are four separate credential uses:

  1. Mount credentials: Basic authentication for that mount's encoder and source metadata updates.

  2. Upstream relay credentials: optional Basic credentials sent to the remote origin; unrelated to the mount's local credential.

  3. HLS producer credentials: Basic authentication for the typed private HLS ingest/publication API.

  4. Management token: Bearer authentication for native /api inspection/control.

For mount and HLS credentials, configure exactly one of secret or secret_file. For management, configure exactly one of token or token_file. Providing both or neither in an enabled scope rejects startup. Optional relay authentication uses a different rule: supply both username and secret_file, or omit both.

Local mount/HLS usernames must be 1–64 bytes, ASCII graphic characters, without whitespace or :. Default usernames are source and hls, respectively. Their passwords must be 16–512 bytes after stripping a final newline. The implementation enforces byte length, not an entropy score; generate unpredictable credentials. Unlike relay passwords and management tokens, local Basic password validation does not require all password bytes to be ASCII graphic. Hexadecimal secrets avoid encoding/interoperability problems and are the recommended example format. A password may contain :; a username may not.

Management tokens must be 32–512 bytes, all ASCII graphic characters without whitespace. Relay upstream passwords must be 1–512 bytes, all ASCII graphic characters without whitespace; they may need to match an origin's short existing password.

Secret loaders strip exactly one trailing LF or CRLF. They do not trim spaces, quotes, a UTF-8 BOM, or extra blank lines. A secret file contains only the secret, not password=..., TOML, JSON or a quoted string. Reads are bounded and oversized content rejects. Files are read at startup, so later edits require restart.

HLS must not reuse any configured mount's password, including a relay mount's local password. Startup checks actual password bytes, regardless of username. Mount-to-mount secret reuse is not prohibited by this check, and the management token is not cross-compared with Basic secrets; use distinct values for all scopes anyway so rotation and disclosure stay contained.

Inline alternatives are accepted:

[[mounts]] path = "/example" username = "source" secret = "REPLACE_WITH_A_GENERATED_SOURCE_SECRET"

The illustrative string is not a production secret. Prefer files because shared configs can then omit actual values. File permissions, owner access, storage and backup handling are deployment responsibilities; the daemon does not itself enforce chmod 600 or encrypt files. Basic and Bearer credentials need an appropriately trusted local/private path or external TLS when transmitted over a network.

Continuous mounts

Each [[mounts]] defines one static source/listener path. Configure 1–64 mounts for continuous delivery. Zero mounts is allowed for an HLS-only server with [hls] enabled. An entirely empty server configuration rejects startup.

path

Required. A unique, canonical path beginning with /, at most 2,048 bytes. Names are case-sensitive. Allowed characters are ASCII letters/digits and / . _ - ~. Nested paths such as /stations/beef are valid.

No root-only /, trailing slash, empty component (//), ./.. component, spaces, Unicode names, query strings or percent-encoded alternatives are accepted as configured mount paths. The reserved roots /api, /admin, /internal, /health, /metrics, /hls and their descendants cannot be used as mounts. /api-radio is a different name and is not a descendant of /api.

The path extension does not select the codec. /beef may carry MP3; /radio.mp3 is just a name. Source MIME and encoded media determine format. There is no per-mount codec, bitrate, station-title or content-type allowlist field.

fallback

Default: omitted, no fallback. An exact configured canonical continuous mount path. Quikcast resolves the requested mount first, then its ordered fallback chain, with at most eight mounts including the primary. Unknown targets, self-reference, cycles, duplicate paths and excess depth reject startup before binding. HLS is excluded. See Fallback mount configuration below for the complete example, routing, limits and recovery behavior.

Fallback consumes the serving mount's listener permit and byte accounting, with one global listener permit. It does not consume the requested mount's permit or bypass capacity/drain errors. Existing listeners stay on their selected serving generation when the primary returns; new requests use the restored primary. Source loss closes existing listeners normally; players reconnect to resolve again.

username

Default: "source". The local encoder/metadata Basic username. Bounds are described in credentials.

secret, secret_file

Exactly one is required. secret is the literal password; secret_file names the startup-loaded password file. No generated default password exists.

[mounts.access]

Default: omitted. Optional nested listener policy with IP/CIDR, User-Agent and country allow/deny lists. It adds restrictions to the global [access] policy and applies to this requested URL before fallback resolution. Source credentials and resource limits remain separate. See Listener access control for every field, examples and limits.

listener_limit

Default: omitted, inheriting [limits].listeners_per_mount. A positive number limits listeners on this mount and must not exceed max_listeners. 0 removes the additional local cap, but global listener/connection limits remain. An explicit override replaces the per-mount default; it is not the minimum of those two values.

[[mounts]] path = "/beef" secret_file = "beef-source" listener_limit = 25 [[mounts]] path = "/talk" secret_file = "talk-source" # Omitted: inherits listeners_per_mount. [[mounts]] path = "/uncapped-locally" secret_file = "uncapped-source" listener_limit = 0

Only one encoder/source generation owns a mount at a time. A second authenticated encoder is rejected; there is no takeover or priority field. Source termination ends its listeners; a fresh source is a new generation and players must reconnect. At most two retained generations are allowed per mount, counting active/closing generations referenced by listeners. Reconnect can reject temporarily if previous generation resources still occupy this bound.

Direct continuous sources accept audio/mpeg (MP3), audio/aac and audio/aacp (AAC ADTS), audio/ogg and application/ogg (supported Vorbis/Opus/Ogg FLAC mappings), and audio/flac (native FLAC). This is a bounded parser/mapping contract, not support for arbitrary Ogg codecs or arbitrary FLAC packaging. MIME aliases such as audio/x-flac are not configured. See the native FLAC and Ogg FLAC reviews for mapping/frame restrictions and evidence.

MP3/AAC listeners can negotiate ICY track metadata. Ogg and native FLAC have no inserted ICY byte blocks. Station headers come from the source/upstream, and source-authenticated /admin/metadata updates generation track state. These are not TOML station fields.

Fallback mounts: configuration and behavior

Set fallback on the mount listeners request. Its value is another configured continuous mount's exact path. The listener keeps its original URL; Quikcast attaches it internally to the first usable source in the chain, without an HTTP redirect.

Complete development example

[limits] max_listeners = 32 listeners_per_mount = 32 [[mounts]] path = "/beef" fallback = "/radio.mp3" username = "source" secret_file = "secrets/beef-source" listener_limit = 2 [[mounts]] path = "/radio.mp3" fallback = "/last.mp3" username = "source" secret_file = "secrets/radio-source" listener_limit = 2 [[mounts]] path = "/last.mp3" username = "source" secret_file = "secrets/last-source" listener_limit = 2

This declares /beef -> /radio.mp3 -> /last.mp3. For a single backup, omit /radio.mp3's fallback line and the /last.mp3 mount. For no fallback, omit the primary's fallback field. Keep any existing root, management, HLS and relay sections when adding these settings to your configuration; merge fields into an existing [limits] section rather than declaring it twice. Secret-file paths resolve relative to the TOML file; create the files with the machine-credential requirements described above.

Mount declarations do not generate audio. Connect an encoder or configure a relay for a mount to supply it. For example, an existing [[relays]] with mount = "/radio.mp3" makes that relay the backup source for /beef; fallback needs no relay-specific field. Dedicated relay mounts keep their normal encoder-admission restrictions.

Routing and startup validation

After passing global and /beef listener access policy, each new GET for /beef tries these candidates in order:

  1. /beef, if its source generation is streaming, uncancelled and initialized sufficiently for its format.

  2. Otherwise /radio.mp3, under the same conditions.

  3. Otherwise /last.mp3.

  4. If none is usable, return 404 Mount unavailable. There is no waiting for a source, and the public error does not disclose the chain.

A source that is Connecting or has incomplete Ogg/native FLAC initialization is not yet usable by a listener. A full usable mount returns the normal 503 capacity error; Quikcast does not try a later mount to bypass its limit. Drain and other errors do not trigger fallback.

Startup validates the entire graph before binding. Targets must be configured canonical continuous paths, using the same case-sensitive path rules as path. Unknown targets, self-reference, cycles, duplicate/ambiguous mount paths, noncanonical targets and chains longer than eight mounts including the requested primary reject startup. HLS paths cannot be fallback targets. Configuration changes require restart.

HEAD first evaluates global and requested-mount access policy, then follows the same availability order and reports the selected serving source's headers. It creates no listener, consumes no listener permit and does not increment fallback admissions. A later GET can select a different source if availability changes.

Primary recovery and serving-source loss

If /beef is unavailable and a listener attaches to /radio.mp3, it stays on that exact /radio.mp3 generation when /beef returns. New requests prefer the restored /beef; existing fallback listeners are not moved back. A request already preparing a usable backup can finish there even if the primary returns concurrently.

If that serving /radio.mp3 source disconnects, its connected listeners end normally. They are not moved to /last.mp3 or a reconnected /radio.mp3 generation. A player reconnecting to its original /beef URL resolves the chain afresh. Each candidate is attempted once per request; a candidate replaced during admission is skipped for that request, even if its replacement is now usable.

A source may connect to /beef normally while its listeners are being served by a backup. Fallback does not replace sources: a second source on an already occupied mount still receives 409 Source already connected. During drain, existing fallback listeners continue under normal drain semantics and new listener admissions are rejected; shutdown closes them.

Limits and reservation budgeting

The serving mount owns the listener permit, generation, ring references, active-listener count and body-byte accounting. A /beef request served by /radio.mp3 consumes /radio.mp3's local permit and one global listener permit; it consumes no /beef listener permit and is not counted twice. Direct and fallback listeners share the backup's cap. Global inbound connection and listener limits remain authoritative.

The development example sets the global listener maximum explicitly because startup reserves against max_listeners, even if each mount has a smaller listener_limit. Omitting [limits] inherits a global maximum of 10,000, which can exceed the default retained-audio budget with multiple mounts. If startup reports retained audio budget cannot cover admission reservations, set a suitable max_listeners and ensure listeners_per_mount does not exceed it, or deliberately increase retained_audio_budget to cover your intended capacity. See the reservation calculation. Fallback bookkeeping reserves an additional 2,048 bytes per listener for requested-mount identity.

Actual format, metadata and inspection

Response Content-Type, station headers, initialization, track state and ICY behavior come from the actual serving generation. Mount extensions do not select formats: /beef can receive an Ogg backup even if its primary normally sends MP3. Quikcast does not convert audio; the player must support the serving format. MP3/AAC retain negotiated ICY insertion, while Ogg/native FLAC do not gain ICY byte blocks. Primary and backup metadata are not merged. Metadata updates still address the actual source's mount.

Authenticated mount inspection exposes fallback as the configured target or null. Listener inspection exposes requested_mount and serving_mount; the existing mount field remains the serving mount. /api/listeners?mount=... filters the serving registry, so inspect /radio.mp3 to find /beef requests currently served there. Counts and bytes are attributed to the serving mount only.

Prometheus and /api/stats expose the fixed unlabeled fallback_listener_admissions_total, incremented once per successful fallback listener admission. Failed attempts, HEAD and direct-primary requests do not increment it. A structured admission log records requested mount, serving mount and generation.

See the dedicated fallback guide and completion review for implementation evidence and concurrency coverage.

Listener access control

Listener access configuration restricts public playback using the resolved client IP, request User-Agent and country from an operator-provided MMDB. It is optional: omitting both [access] and [mounts.access] keeps existing unrestricted listener behavior. An empty table or empty list adds no restriction. Listener access is separate from source, HLS producer and management credentials.

Global and mount tables

[access] defines the server-wide policy. [mounts.access] adds restrictions to the most recently declared [[mounts]] entry. Every continuous listener must pass global policy AND requested-mount policy. A mount cannot override a global deny, disable the global policy or bypass a global allow requirement.

All six list fields below are available in both tables. Their defaults are []. geoip_database is available only in the global table and defaults to omitted. There is no enabled, override, disable_global, rule-order or boolean-expression setting.

Place root settings before named tables. Put each [mounts.access] immediately after its owning mount's fields; begin the next mount with another [[mounts]]. Once [mounts.access] is open, listener_limit, fallback and source credential fields would be interpreted as access keys and rejected. Do not repeat [access] to add another field; merge into the existing table.

ip_deny

Default: []. At most 256 entries per policy. A matching address or CIDR rejects the request, even if it also matches ip_allow. Matching uses typed IP/network values, not string prefixes.

[access] ip_deny = [ "203.0.113.10", # Exact IPv4 address. "198.51.100.0/24", # IPv4 network. "2001:db8::1", # Exact IPv6 address. "2001:db8:dead::/48" # IPv6 network. ]

The example documentation addresses illustrate syntax; replace them with the addresses/networks appropriate to your deployment. Bare addresses behave as /32 for IPv4 and /128 for IPv6. Ordinary prefix ranges are 0–32 and 0–128. Host bits in a CIDR are masked: 192.0.2.9/24 and 192.0.2.0/24 represent the same network. Invalid IPv4, malformed IPv6, invalid prefixes, hostnames and port-bearing values reject startup.

ip_allow

Default: []. At most 256 entries per policy, with the same address/CIDR syntax and validation as ip_deny. A nonempty list requires the resolved client IP to match at least one entry. An empty list imposes no positive IP requirement.

[[mounts]] path = "/lan" secret_file = "secrets/lan-source" [mounts.access] ip_allow = ["10.0.0.0/8", "192.168.0.0/16", "fd00::/8"]

If the global policy also has ip_allow, the client must match the global list and this mount's list. The two lists are not combined into a larger alternative allow set. IP allow does not bypass country or UA restrictions.

Resolved IP, trusted proxies and mapped IPv4

IP policy and GeoIP lookup use the same canonical client address produced by the existing trusted_proxies resolver. With no trusted peer, this is the socket peer's IP. With a legitimately trusted proxy, it is the validated address obtained by walking the forwarded chain. A forwarded header from an untrusted socket peer is ignored; access control does not have a second forwarded-IP parser.

IPv4-mapped IPv6 peers and forwarded hops normalize to IPv4. Thus ::ffff:192.0.2.1 matches 192.0.2.0/24. A mapped rule such as ::ffff:192.0.2.0/120 normalizes to 192.0.2.0/24; mapped rule prefixes 96–128 become IPv4 prefixes 0–32. Mapped rules with prefixes below 96 reject startup. Ordinary IPv6 rules apply to IPv6 identities; mapped identities use IPv4 rules after normalization.

Trust only your actual proxy infrastructure and configure it to supply the real client chain correctly. A broad or incorrect trust configuration can change the identity used by IP and country restrictions. See TLS, reverse proxies and forwarded addresses for header bounds and chain validation.

user_agent_deny

Default: []. At most 256 rules per policy. Any matching deny rejects, including a UA that also matches an allow rule. Each rule requires one explicit mode:

  • exact:<value> compares the entire User-Agent.

  • prefix:<value> compares its beginning.

  • substring:<value> searches anywhere in the value.

[access] user_agent_deny = [ "exact:bad-monitor", "prefix:curl/", "substring:python-requests" ]

Matching is case-sensitive. prefix:curl/ matches curl/8.0 but not Curl/8.0. Mode names are also case-sensitive. Values must contain 1–256 bytes and no control characters. Empty values, missing/unknown modes and oversized values reject startup. Plain strings are not implicit substring rules; regex and wildcard modes are not supported. Rule parsing happens once during startup.

A missing or non-text User-Agent does not match a deny rule, so deny-only UA configuration permits its absence unless another category rejects. Runtime matching uses the complete already-parsed header within the existing 16 KiB aggregate HTTP header ceiling. It does not use the truncated copy retained for management inspection.

user_agent_allow

Default: []. At most 256 rules per policy, using the same exact/prefix/substring syntax, case behavior and value bounds. With a nonempty list, at least one rule must match. Missing or non-text User-Agent fails this requirement. An empty list requires no UA match.

[[mounts]] path = "/players" secret_file = "secrets/players-source" [mounts.access] user_agent_allow = ["exact:station-player", "prefix:RadioPlayer/"] user_agent_deny = ["substring:unsupported-build"]

A request with RadioPlayer/1 unsupported-build is denied despite matching the prefix allow rule. User-Agent is client-controlled and spoofable; filtering is an access/compatibility feature, not identity authentication.

geoip_database

Default: omitted. An operator-installed MaxMind-compatible MMDB with usable country.iso_code records. Configure it only in [access]; mount tables do not accept a database path.

[access] geoip_database = "/etc/quikcast/GeoLite2-Country.mmdb"

A relative path, such as geoip_database = "geoip/country.mmdb", resolves beside the configuration file. With a config at /etc/quikcast/quikcast.toml, that example reads /etc/quikcast/geoip/country.mmdb. Absolute paths remain absolute. Tilde and environment-variable expansion are not performed.

Any nonempty global or mount country list requires this shared database. IP and UA policies work without one. If a path is configured, Quikcast opens and validates it even if no country restrictions are active. Missing/unreadable files, corrupt MMDB structures, incompatible country data, invalid record country codes, and databases without any usable country.iso_code records reject startup before binding.

The file is limited to 134,217,728 bytes (128 MiB). Country-schema validation permits at most 2,000,000 network records. The file is loaded once into a reusable immutable reader at startup. Admission uses in-memory lookup; it does not reopen files, contact MaxMind, call a GeoIP API or perform DNS queries. Global and mount evaluation share one lazy lookup result when both need it.

The operator owns database acquisition, licensing, installation, readability and updates. Quikcast performs no automatic downloads, refreshes, file watching or hot reload. Prepare a replacement file and restart to use it; replacing the file beneath a running process leaves its already-loaded reader unchanged. GeoIP is approximate and depends on database coverage, accuracy and freshness; it is not an identity or legal/compliance guarantee.

countries_deny

Default: []. At most 249 entries per policy. Each value must be an actual ISO 3166-1 alpha-2 country code. Lowercase configuration is accepted and normalized to uppercase: "gh" becomes "GH". Malformed and unrecognized codes reject startup; examples include "USA", "1A" and "ZZ".

[access] geoip_database = "/etc/quikcast/GeoLite2-Country.mmdb" countries_deny = ["US", "RU"]

A successfully resolved country in this list is denied. A country outside the list passes this category. Unknown and local addresses pass deny-only country policy; genuine runtime lookup/decoding failures fail closed. Other global/mount categories still apply.

countries_allow

Default: []. At most 249 entries per policy, with the same ISO validation and normalization as countries_deny. A nonempty list requires a successfully resolved country present in the list. A different country, absent country or local address fails. Deny wins if the resolved country appears in both country lists.

[access] geoip_database = "/etc/quikcast/GeoLite2-Country.mmdb" [[mounts]] path = "/radio" secret_file = "secrets/radio-source" [mounts.access] countries_allow = ["GH", "NG"]

Country lookup reads country.iso_code. It does not substitute registered_country, continent, city, subdivision or ASN. A database entry without a country is unknown even if other geographic fields exist.

Unknown, local and failed lookups

The evaluator distinguishes a found country, an address absent from MMDB or a record without a country, a local address, and an actual runtime lookup failure.

Private, loopback, link-local, unspecified and multicast addresses are classified as local without MMDB lookup. This also includes IPv4 broadcast, the IPv4 zero network, shared address space 100.64.0.0/10, and IPv6 unique-local addresses. IP rules still match these addresses normally.

  • Found country: deny-list membership rejects; any nonempty allow list requires membership.

  • Unknown address/country: a nonempty country allow list rejects; deny-only country policy passes.

  • Local address: the same allow/deny-only behavior as unknown; there is no implicit bypass.

  • Runtime lookup/decoding failure: any active country policy rejects, including deny-only policy. The internal reason distinguishes this from normal absence.

For example, ip_allow = ["10.0.0.0/8"] combined with countries_allow = ["GH"] still denies a private 10.x client. Passing the IP allow requirement does not establish a country or waive its requirement. For a LAN-only mount, use IP restrictions without a country allow list unless rejecting local clients is intentional.

Complete global, mount and fallback example

This example puts a global UA deny on every public playback request and a country allow requirement on the /main URL. The backup has its own direct-listener IP restriction:

listen = "127.0.0.1:8000" admin_listen = "127.0.0.1:9090" trusted_proxies = [] [access] geoip_database = "/etc/quikcast/GeoLite2-Country.mmdb" ip_deny = ["203.0.113.0/24"] user_agent_deny = ["exact:blocked-client", "substring:python-requests"] [[mounts]] path = "/main" fallback = "/backup" secret_file = "secrets/main-source" listener_limit = 32 [mounts.access] countries_allow = ["GH", "NG"] [[mounts]] path = "/backup" secret_file = "secrets/backup-source" listener_limit = 32 [mounts.access] ip_allow = ["10.0.0.0/8", "192.168.0.0/16", "fd00::/8"]

Create both source credential files before startup; source credentials are still required on mounts whose listeners are restricted. This example assumes the MMDB already exists. No proxy is trusted by default. If a controlled reverse proxy sits in front of the server, set the root trusted_proxies list to that infrastructure's actual addresses according to the proxy guide.

A GET or HEAD for /main passes global policy and /main policy before fallback resolution. If /backup supplies its media, /backup's direct-listener policy is not applied to that internal serving step. A direct /backup request must pass global policy and /backup's policy. The requested URL owns access policy; the selected serving mount owns media, listener permits and byte accounting. This separation prevents fallback from bypassing /main restrictions while preserving backup resource limits.

Request order, public responses and lifecycle

For a configured continuous mount, Quikcast checks global policy first and mount policy second. Within each policy it checks IP deny, IP allow, country deny/allow, UA deny and UA allow. Any failure stops evaluation. An IP rejection skips country lookup. There are no ordered operator-defined firewall rules.

Denied public listeners receive HTTP 403 Forbidden with the generic GET body Forbidden followed by LF. HEAD has an empty body. Responses do not identify an IP rule, UA rule, country, database status or policy scope. Denials occur before creating listener records, joining rings or consuming global/serving-mount listener permits. Existing inbound connection limits and response-byte accounting still operate normally.

GET and HEAD evaluate the same access policy. An allowed HEAD retains existing source/fallback metadata behavior and creates no listener lease or successful listener/fallback admission count. Allowed GET still needs a usable source and available capacity; passing access policy does not guarantee a 200 response.

Drain preserves existing routing precedence: GET on a configured continuous mount returns 503 Server draining before ACL evaluation. HEAD remains metadata-only during drain and still evaluates access policy. Already admitted sessions retain normal lifecycle behavior. Configuration and database changes require restart; there are no mutable ACL APIs.

Global policy also applies to public HLS GET/HEAD playlists and segments. Per-continuous-mount policy never applies to unrelated HLS streams. HLS OPTIONS/CORS behavior remains unchanged; already published HLS playback remains available during drain when global policy passes. There is no HLS-specific access table.

Listener ACLs do not apply to source ingest, /admin/metadata, native management, private HLS producer operations or internal relay connections. Each keeps its existing authentication/trust model. A relay's downstream public listeners do evaluate the requested mount's ACL normally.

Deduplication, bounds and inspection

Each IP and UA allow/deny list has its own 256-entry ceiling; each country allow/deny list has its own 249-entry ceiling. Global and every mount are bounded independently. Limits count supplied entries before deduplication, so repeating a rule does not permit exceeding a list ceiling. Typed equivalent IP networks, identical UA rules and normalized country codes are deduplicated at startup. Overlapping networks remain separate.

The full configuration still has its 128 KiB file ceiling. The 256-byte UA value ceiling, 128 MiB database ceiling and 2 million country-validation-record ceiling are fixed implementation limits, not additional TOML settings.

Authenticated /api/server exposes the listener-access-control capability, global access inspection, geoip_loaded and mount_access_enabled. /api/mounts and individual mount inspection include each mount's own access object. The sanitized object contains ip_allow_rules, ip_deny_rules, user_agent_allow_rules, user_agent_deny_rules, countries_allow and countries_deny. IP/UA rule contents and the database filesystem path are omitted. Inspect both global and mount objects to understand the cumulative continuous policy.

Three fixed unlabeled counters are available through private /metrics: listener_access_denied_ip_total, listener_access_denied_geo_total, and listener_access_denied_user_agent_total. Access denials do not increment active listeners, successful listener admissions, fallback admissions or capacity/authentication rejection counters. These counters aggregate global/mount and public continuous/HLS rejection categories without IP, UA, country or mount labels.

Debug-level structured rejection logs contain the bounded requested path and typed reason. This feature does not log client IP, full UA or country values, and it does not log successful decisions. The normal quikcast=info filter suppresses this detail. Debug output uses the existing bounded nonblocking sink; it is diagnostics, not a durable audit trail.

For the milestone's offline fixture tests and local validation evidence, see the listener access guide.

Continuous resource limits

All fields below belong to [limits]. Omitted fields use the stated default. Bounds are admission/safety controls; they do not establish achievable throughput or production capacity.

ring_bytes

Default: 4,194,304 bytes (4 MiB). Positive maximum retained encoded audio bytes per source-generation ring. There is no separate hard maximum in the validator; checked reservation arithmetic and retained_audio_budget must still accommodate it. A value causing arithmetic overflow or insufficient budget rejects startup.

Retention is also constrained by ring_entries. Media leaves the ring when either applicable bound is reached. Slow listeners that fall behind retained data terminate; rings are not unbounded queues and do not hold back healthy source publication.

ring_entries

Default: 4,096. Range 1–65,536. Maximum ring entry slots per generation. Entries are bounded published media blocks, not necessarily one codec frame or one second each. Entry bookkeeping also consumes the admission reservation.

join_bytes

Default: 65,536 bytes (64 KiB). Range 0 through ring_bytes. Controls the retained audio window considered for a new listener. It is not a duration in milliseconds. Codec boundaries/initialization still decide the actual safe start; startup/readiness or safe-frame conditions can make a join unavailable or wait for an appropriate boundary. 0 requests joining at the current live position with the codec's required initialization/boundary rules; it does not disable format validation.

max_connections

Default: 12,000. Range 1–100,000. Shared active inbound TCP connection cap across public and admin bindings. Includes source, listener, API/health and HLS HTTP connections. There is no reserved admin capacity under full saturation. Outbound relay connections are governed separately by configured relay workers, not this inbound cap.

max_handshakes

Default: 128. Range 1 through max_connections. Bounds concurrently pending inbound initial request/header handshakes. It is not a TLS handshake limit: inbound TLS happens at the external proxy. Excess admission is refused rather than placed into an unlimited handshake queue.

max_listeners

Default: 10,000. Range 1 through max_connections. Global cap on active continuous listener leases across mounts. HLS object requests use HTTP connection/resource accounting; they are not continuous listener leases. This value also participates in startup reservation calculation even when configured local caps are smaller.

listeners_per_mount

Default: 10,000. Range 0 through max_listeners. Default continuous listener cap per mount. 0 means no additional local cap; the global cap still applies. A mount's explicit listener_limit replaces this default.

retained_audio_budget

Default: 1,073,741,824 bytes (1 GiB). Must cover conservative continuous admission reservations. There is no independent fixed minimum/maximum: required size is calculated from mount count, two retained generations per mount, ring slots/bytes, ingress/metadata/parser staging, FLAC buffers and maximum listener-held references.

The current validator calculates:

generation = ring_entries × ring_entry_reservation + ring_bytes + 16,384 + 8,192 + ICY_MAX_BLOCK + OGG_MAX_PAGE + 2 × OGG_MAX_HEADERS + flac_initialization_bytes + flac_frame_bytes + 16 + 2,048 listener = 2 × 16,384 + ICY_MAX_BLOCK + max(OGG_MAX_HEADERS, flac_initialization_bytes) + 4,096 + 2,048 required = 2 × mount_count × generation + max_listeners × listener

Current constants are ICY_MAX_BLOCK = 1,041, OGG_MAX_PAGE = 65,307, and OGG_MAX_HEADERS = 65,536 bytes. ring_entry_reservation = 2 × sizeof(Rust Entry) + 128, so that component is build/architecture-dependent. All operations are checked for integer overflow. These are explicit accounting reservations, not an allocator/kernel/proxy/whole-process RSS guarantee.

The additional 2,048 bytes per listener reserve its retained requested-mount identity alongside the serving identity. This applies to direct and fallback listeners; a previously tight budget may need increasing. The formula includes FLAC allowances for each continuous generation even when the currently connected sources use MP3. Lower per-mount listener caps do not reduce its max_listeners term. Increasing mount count, ring capacities, global listeners or FLAC buffers can make an otherwise unchanged budget reject startup. HLS and relay-worker budgets are separate; they do not increase this budget implicitly.

relay_allocation_budget

Default: 33,554,432 bytes (32 MiB). Must be at least 2,097,152 bytes × configured relay count. At most 16 relays exist. Disabled relays count too. With zero relays, zero passes this particular reservation check. This is separate relay worker/DNS/HTTP/TLS staging admission accounting, not remote audio storage or a whole-process memory limit.

header_timeout_ms

Default: 5,000 ms. Range 1–3,600,000 ms. Inbound request/header deadline. Applies to public/admin HTTP handling; a slow/incomplete header is refused. It is distinct from the relay's outbound header timeout.

source_timeout_ms

Default: 10,000 ms. Range 1–3,600,000 ms. Direct continuous source body inactivity deadline. There is no total healthy stream duration limit. Relay audio progress uses its own audio_timeout_ms. HLS upload timing has separate fixed rules described below.

write_timeout_ms

Default: 10,000 ms. Range 1–3,600,000 ms. Socket write-stall deadline, also used for bounded direct responses. An idle stream with no pending audio is not inherently a slow-reader violation; pending transport data that cannot progress is. This is not a maximum continuous listening duration.

shutdown_timeout_ms

Default: 10,000 ms. Range 1–3,600,000 ms. Deadline for the supervised shutdown/join phase after cancellation. Remaining supervised connections are aborted when that phase reaches its deadline. This is not a drain duration or a guaranteed wall-clock bound for every surrounding OS/service-manager action.

Native FLAC limits

These fields also belong to [limits] and affect continuous admission budgeting. They do not select FLAC as a mount's format and do not configure Ogg FLAC packet limits.

flac_initialization_bytes

Default: 65,536 bytes (64 KiB). Range 42–262,144 bytes. Total native FLAC initialization allowance: fLaC marker, all metadata block headers and payloads, including artwork/comments/seek tables. Oversized declared metadata fails the source's bounded initialization path. Increasing this value increases generation staging/cache and potentially listener-held initialization reservation.

flac_frame_bytes

Default: 1,048,576 bytes (1 MiB). Range 16–4,194,304 bytes. Maximum native FLAC frame staging allowance. The parser separately reserves 16 bytes for successor-header lookahead. A frame that cannot be resolved within that bound fails locally. This is bytes per unresolved encoded frame, not a PCM/sample/block-size setting.

Native FLAC caches accepted initialization for each generation and starts late listeners at confirmed frame boundaries. It does not rewrite original tags, file duration/MD5 or seek tables for a midstream listener. See the review for required numbered frames, stable properties and checksum/header handling. Larger limits permit larger input envelopes; they do not enable arbitrary format changes, ID3-prefixed input or concatenated native files.

Ogg FLAC uses the shared Ogg initialization/page machinery and fixed mapping/packet bounds, including a 64 KiB initialization ceiling. Raising native FLAC limits does not enlarge those Ogg limits.

Continuous relays

Every [[relays]] must reference a matching [[mounts]] path. At most 16 relays, with exactly one per unique local mount. A relay-owned mount cannot accept an external encoder even when its relay is stopped or enabled = false.

mount

Required in practice; its omitted default is an empty string and startup rejects it. Must exactly match a configured continuous mount. It is the local listener URL path, not the upstream URL path.

url

Required in practice; empty default rejects startup. Absolute HTTP or HTTPS continuous stream URL, at most 2,048 bytes. Default ports are 80 and 443. Explicit port range is 1–65,535. Hostname/IP syntax must pass the endpoint/TLS server-name validation; host length is at most 253 bytes. Bracket IPv6 URL literals correctly.

Queries are allowed when required by the upstream. Fragments, URL userinfo (user:pass@host), whitespace/non-ASCII graphic URL bytes, unsupported schemes and invalid ports reject. Supply upstream credentials separately. Redirects are not followed: configure the final stream URL. HLS playlist URLs are not continuous relay inputs.

Known self-targets matching local configured/actual bindings or resolved local addresses reject. Proxy-mediated/multi-server loops cannot be inferred completely; keep the topology acyclic. Remote private addresses are valid for intentional private origins.

username, secret_file

Default: both omitted, meaning anonymous upstream access. Supply both for Basic authentication. Upstream usernames have the same 1–64 ASCII-graphic/no-colon constraint; upstream passwords have the different 1–512 ASCII-graphic-byte constraint. The file resolves beside the config. There is no inline relay secret field. The local mount still needs its own local secret even for an anonymous remote origin.

[[mounts]] path = "/remote" secret_file = "remote-local-metadata" [[relays]] mount = "/remote" url = "https://origin.example.com/live" username = "remote-user" secret_file = "origin-password"

enabled

Default: true. Whether the relay should run at process startup. false starts it stopped; native management can start it later. Runtime stop/start intent is volatile. Restart re-applies this static value. A stopped relay still reserves its dedicated mount and worker-budget allowance.

connect_timeout_ms

Default: 10,000 ms. Range 1–3,600,000 ms. One overall outbound deadline covering DNS lookup, bounded sequential address connection attempts and TLS handshake. It is not a separate allowance for each resolved address.

header_timeout_ms

Default: 5,000 ms. Range 1–3,600,000 ms. Deadline for the outbound request/response-header exchange after connection. Independent of [limits].header_timeout_ms.

audio_timeout_ms

Default: 10,000 ms. Range 1–3,600,000 ms. Inactivity deadline measured by encoded audio progress. Metadata/body traffic alone cannot indefinitely keep a stalled audio attempt alive. A healthy relay has no total connection-duration limit.

initial_backoff_ms

Default: 1,000 ms. Range 1–3,600,000 ms, and must not exceed max_backoff_ms. Initial retry delay after failure.

max_backoff_ms

Default: 60,000 ms. Range 1–3,600,000 ms. Retry-delay ceiling. Nominal delays double after failures until capped; jitter is also clamped within initial/max bounds.

stable_connection_ms

Default: 30,000 ms. Range 1–3,600,000 ms. Connection lifetime that qualifies an attempt as stable and resets nominal backoff for a later failure. Brief connect/disconnect cycles do not repeatedly reset to the fastest retry delay.

jitter_percent

Default: 20. Range 0–50. Retry-delay variation around the nominal delay, still bounded by initial/max. 0 removes variation. This is a percentage integer, not 0.2 or a string.

Relay wire, metadata and TLS behavior

Only successful HTTP status 200 continuous audio responses are admitted. Remote redirects/errors/HTML/compression/untrusted framing fail that relay locally. Legacy ICY 200 OK status syntax is not accepted. HTTP/HTTPS relays have separate transport/parser bounds; native audio/flac is explicitly rejected on relay response admission. Ogg payloads use the shared current parser, but recent Ogg FLAC source implementation is not evidence of a separately qualified Ogg FLAC relay/client deployment; do not infer new interoperability guarantees from it.

Outbound HTTPS verifies certificate chain and hostname/IP using bundled WebPKI roots. There are no config fields for disabling verification, a custom/private CA bundle, client certificates or TLS versions. Native inbound TLS is a separate, external proxy responsibility.

Relays request identity content encoding and ICY metadata. Valid remote MP3/AAC metadata is separated from audio, normalized and serialized anew for local listeners. Ogg metadata stays in-band. Bad optional station/text metadata is ignored or counted; corrupt framing/truncated ICY ends the attempt rather than guessing audio boundaries.

Each relay has one supervised active attempt and bounded retry. When an attempt ends, its source generation and listeners end. A later connection starts a fresh generation; listeners reconnect. Mount fallback can route a new request to another configured continuous mount while this relay is unavailable. There is no seamless listener migration, priority or multi-upstream relay failover config.

Native relay operations on the admin binding use the management Bearer token:

  • GET /api/relays: inspect all configured relays.

  • GET /api/relays/radio.mp3: inspect this relay.

  • DELETE /api/relays/radio.mp3: stop and suppress retry.

  • POST /api/relays/radio.mp3: start/reconnect asynchronously.

  • Add ?revision=N to POST/DELETE for a control-revision guard; stale guards return 409.

Native source deletion on a relay-owned source also suppresses automatic retry. Drain prevents new attempts/reconnects but preserves an already connected generation until it fails or shutdown occurs. See the full relay contract for metadata/DNS/framing details and error categories.

HLS configuration and producer responsibilities

[hls] enables a separate typed, in-memory MPEG-TS subsystem. It needs no continuous mounts and cannot derive HLS from /beef or /radio.mp3. It does not watch FFmpeg output directories or accept arbitrary .m3u8 uploads.

username

Default: "hls". Private producer Basic username, using the local 1–64-byte username rules.

secret, secret_file

Exactly one required whenever [hls] is present. Password length 16–512 bytes. Must differ from every configured mount secret. These authenticate HLS producer operations, not listener playback or native /api access.

max_streams

Default: 16. Range 1–64. Maximum registered HLS stream identities.

renditions_per_stream

Default: 8. Range 1–16. Maximum renditions for a stream.

max_renditions

Default: 64. Range 1–128. Global rendition bound. Per-stream count, global count and allocation reservations all apply independently; the global number need not equal max_streams × renditions_per_stream.

max_segment_bytes

Default: 2,097,152 bytes (2 MiB). Range 188–16,777,216 bytes. Maximum individual finite MPEG-TS upload. This is a safety ceiling, not an expected segment size or target bitrate. Actual media must consist of valid whole 188-byte packets under the supported transport/PSI contract, so an arbitrary byte count inside the config range does not make every file size valid.

max_segments

Default: 1,024. Range 16–4,096. Per-rendition segment/retired-identity slot ceiling, shared by current objects and retired sequence bookkeeping. Advertised/staging/grace objects and retired sequence rules can consume slots. It is not the advertised playlist length. The external producer's window_segments is a separate rendition-registration input.

max_uploads

Default: 4. Range 1–16. Concurrent HLS upload admission bound. It does not change the maximum public segment downloads or continuous listeners. Public HTTP downloads remain subject to shared connection limits and charged retained objects.

rendition_budget

Default: 134,217,728 bytes (128 MiB). Per-rendition allocation reservation ceiling. Must not exceed allocation_budget, and at startup must cover at least:

8 × (max_segment_bytes + 128) + 4 × 262,144

Actual rendition registration performs stricter window/registry reservation checks, so satisfying startup's minimum does not promise acceptance of any arbitrary window size.

For a registered window W, 2 × W + 1 + max_uploads must fit max_segments; W must also be at most max_segments / 2. Its minimum reservation additionally covers those maximum-sized objects, four playlist buffers and rendition segment/retired-index bookkeeping. Object promises are not broken to admit extra work; admission can return 503 instead.

allocation_budget

Default: 536,870,912 bytes (512 MiB). Global HLS allocation ceiling, at most 4,294,967,296 bytes (4 GiB), and at least rendition_budget. It is independent of continuous retained-audio and relay-worker budgets. Actual registry/rendition/object allocations must fit it; count maxima are not simultaneous guaranteed capacities.

cors_origin

Default: omitted, disabling HLS CORS headers/preflight support. Optional string of at most 256 bytes, either "*" or one exact HTTP/HTTPS origin such as "https://player.example.com" or "http://localhost:5173".

No path, trailing slash, query, fragment, userinfo, whitespace, comma-separated origin list, or wildcard subdomain expression. "https://player.example.com/" rejects; use "https://player.example.com". This applies to public HLS responses, not to continuous audio, source credentials or management authorization. With CORS disabled, HLS OPTIONS returns 405; ordinary GET/HEAD delivery still works.

External producer inputs are not TOML settings

The producer registers stream/rendition identities on the private binding under /internal/v1/hls/streams/STREAM. Rendition JSON supplies:

{"target_duration":4,"window_segments":6,"bandwidth":256000,"codecs":"mp4a.40.2"}

target_duration is fixed for a rendition generation, 1–60 seconds. window_segments is 3–256, subject to the extra configured slot/budget checks. bandwidth is a positive u32 declaration, and codecs is bounded to 128 ASCII letters/digits plus . , -. Quikcast does not infer peak bitrate or decode codec properties; the producer owns truthful declarations, durations and continuity.

Segment upload uses both X-HLS-Generation and X-HLS-Rendition-Generation, Content-Type: video/mp2t, X-HLS-Duration-Ms, and optional X-HLS-Discontinuity: 0|1. Publication JSON references consecutive uploaded sequences and includes the expected playlist revision. Master publication references ready renditions and has its own revision. There is no TOML stream_name, hls_time, playlist_path, target_duration, segment directory or FFmpeg command field.

MPEG-TS uploads are validated for packet/initial PAT/PMT structure, not decoded audio/video semantics. Arbitrary raw AAC files, fMP4/CMAF/init objects and encrypted assets are not accepted as this asset format. External FFmpeg can produce AAC-in-MPEG-TS HLS, but a separate producer adapter must upload segments and publish descriptions.

Public URL shapes are /hls/STREAM/master.m3u8, /hls/STREAM/RENDITION/index.m3u8, and generation-qualified .ts object URLs generated in the playlist. GET/HEAD and ETags are supported. Byte Range requests return 416; known FFmpeg whole-object playback uses -seekable 0 -http_seekable 0 -http_persistent 0. Required player compatibility must be checked on its actual request behavior.

Uploaded unpublished staging expires after 30 seconds. Advertised objects cannot expire while advertised. Removed objects stay readable for their own duration plus the longest playlist duration that advertised them. A pruned live window must cover at least three target durations. ENDLIST does not itself delete the final advertised objects; deletion hides playlists and retains promised grace objects. All HLS state is process-local and volatile across restart.

HLS upload inactivity is 10 seconds, with a 30-second total upload/admission deadline. These are fixed HLS rules, not [limits].source_timeout_ms. JSON/playlist descriptions are capped at 262,144 bytes. See the typed HLS API contract for complete upload/publication/end/delete operations.

Management, health and metrics

[management] has only two accepted keys:

token

Optional inline Bearer token. Exactly one of this and token_file is necessary if [management] is present. 32–512 ASCII graphic bytes with no whitespace. No default token exists.

token_file

Optional path to the generated Bearer token; resolves beside the config and strips one final LF/CRLF. Exactly one of this and token must be provided. Editing the file does not rotate the token until restart.

The native /api routes require Authorization: Bearer TOKEN on the admin binding. Source/HLS Basic credentials do not authorize them. Health/metrics remain private-binding routes without native Bearer authentication. HLS producer routes use their separate Basic credential and do not require [management] to be enabled.

With a file-based token, an interactive curl check can keep the token out of command arguments by reading headers from stdin:

{ printf 'Authorization: Bearer '; cat secrets/local-example/management-token; } | curl --fail --silent --show-error --header @- \ http://127.0.0.1:9090/api/server

Examples above assume the generated LF-terminated file. Protect shell traces/logs; do not run credential commands with shell tracing enabled.

Useful private routes include /api/server, /api/stats, /api/mounts, /api/sources/beef, /api/listeners?mount=%2Fbeef, /api/relays, /api/hls, /health/live, /health/ready, and /metrics. Native API snapshots do not provide arbitrary config editing or relay URL creation. See the management API reference for request methods, pagination, identities and revision guards.

Readiness means configuration/bindings are serving and the server accepts new work. It does not require a live encoder, healthy relay, or an HLS publication. Source/listener/relay identities and counters are ephemeral. Native management has no privileged connection reservation when inbound capacity is saturated.

TLS, reverse proxies and forwarded addresses

Inbound TLS is terminated by an external proxy. Keep Quikcast's public upstream bound to loopback when the proxy shares its host; keep the admin binding separate. Do not send a public catch-all proxy to admin_listen.

For an HTTP proxy, source uploads and listener responses require buffering disabled, stream caching disabled and suitable long activity timeouts. The local Nginx rehearsal used the following directives:

proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Connection ""; proxy_set_header X-Forwarded-For $remote_addr; proxy_request_buffering off; proxy_buffering off; proxy_cache off; proxy_read_timeout 1h; proxy_send_timeout 1h;

Expose explicit continuous mount locations and public /hls/ delivery. Keep private /api, /internal, /health and /metrics outside public forwarding. Public /admin/metadata is an authenticated source metadata operation, distinct from the private native API; only forward it if the encoder's metadata workflow requires it. Do not describe it as an unauthenticated administration console.

Set upload body-size policy deliberately: an endless source is not a small finite file upload. The rehearsal's streaming location used client_max_body_size 0. This is an external Nginx setting, not a Quikcast config key.

Nginx's proxy and certificate directives are documented in its proxy module and TLS module. Our test used an isolated config and explicitly trusted temporary certificate. It did not configure a production certificate/domain or install system trust.

HTTP chunked PUT was validated through the proxy. A legacy SOURCE method or unframed encoder handshake is not automatically supported by an HTTP proxy that accepts ordinary HTTP only. Check actual BUTT/Liquidsoap behavior through the intended path; where necessary, use a properly configured TCP/TLS transport rather than inferring compatibility from HTTP PUT. Native inbound TLS and PROXY protocol are not implemented configuration options.

Only X-Forwarded-For influences client-IP inspection. The immediate socket peer must match trusted_proxies; otherwise the header is ignored. Header bounds: exactly one field, at most 1,024 bytes, at most 16 comma-separated plain IP addresses. Invalid, empty, oversized, duplicate or port-bearing values fall back to the socket peer.

For trusted peers, walk the chain from right to left while the current hop is trusted, stopping at the first untrusted address. IPv4-mapped IPv6 peers and forwarded hops normalize to IPv4, so an IPv4 CIDR also matches the equivalent mapped address. The canonical resolved identity is used for inspection and listener IP/country policy. No Forwarded, X-Real-IP or PROXY-protocol interpretation exists. Configure the proxy to replace hostile incoming XFF or append the actual peer appropriately; broad trust networks can create spoofed inspection identities.

Drain, shutdown, restart and rotation

Drain is an authenticated native API operation, not a TOML switch:

{ printf 'Authorization: Bearer '; cat secrets/local-example/management-token; } | curl --fail --silent --show-error --header @- --request POST \ http://127.0.0.1:9090/api/server/drain

Drain is one-way within a process. Repeating it is idempotent; there is no undrain/resume API. It does not schedule an automatic exit.

  • Readiness becomes 503; liveness/inspection stay available.

  • New continuous sources/listeners and new HLS producer mutations/uploads reject.

  • Admitted continuous source/listener sessions continue under their ordinary EOF/timeout/control rules.

  • An already connected relay continues; after it ends, drain suppresses new retry attempts. Manual reconnect rejects.

  • Already published HLS objects stay readable, but no new publication can advance the stream; playback eventually reaches retained content's edge.

  • A previously admitted HLS upload can finish according to its existing ownership; drain rejects new producer work at admission.

Ctrl+C/SIGINT or SIGTERM initiates real shutdown: close listeners, cancel owned work, close generations and join supervised tasks within the shutdown phase deadline. A normal restart begins accepting again, reloads static credentials/config, and starts relays according to enabled.

Restart clears active listener/source sessions, runtime stop intent, counters, opaque identities, cached generations and HLS assets. Encoders and players reconnect; an external HLS producer must register/upload/publish again. The test HLS adapter generates a new stream name per invocation, so it prints a new playback URL rather than silently restoring the old one.

There is no live reload, SIGHUP reload, restart-free secret rotation or persisted runtime control state. To rotate a credential, prepare the new file/config and update affected clients, then restart during the intended maintenance procedure. Keep the old value until the coordinated transition if recovery requires it; do not publish actual values in reports.

Startup rejection and troubleshooting

Configuration can parse correctly and still fail semantic validation, credential loading, logging initialization or OS binding. The CLI exits unsuccessfully rather than serving with an invalid partial setup. There is no automatic fallback to a different config.

Relay has no matching mount

Every relay needs an exact matching [[mounts]] path. A relay mount = "/f5" with only path = "/radio.mp3" rejects. Fix the local path pairing; the upstream URL need not share the same path.

The repository's editable config/quikcast.example.toml may contain local development edits. Do not assume that file is a complete validated deployment config. The complete example in this guide is independently checked; create its referenced credentials before using it.

Credential cannot be loaded or does not authenticate

Check the path relative to the actual config directory, file readability, exactly-one-field rule, password/token length and extra newline/quote/BOM bytes. Use the correct scope: source Basic, upstream Basic, HLS Basic and management Bearer are different. A relay mount rejects encoder ownership even with its correct local source password.

Budget or capacity rejected at startup

Check global listener/connection relationships, join_bytes <= ring_bytes, ring entries, FLAC bounds and deadline ranges. Recalculate conservative reservations after adding mounts/listeners or changing buffers. HLS budget and window admission rules are separate from continuous audio reservations. Overflow rejects rather than wrapping.

Port cannot bind

Stop the existing process or choose distinct unused bindings. Hostnames/URLs are not socket-address settings; IPv6 literals need brackets. Admin must be loopback. Non-loopback public binding does not enable TLS or configure a firewall.

Stream path returns 404

Confirm exact case-sensitive configured path, active source/relay state and codec initialization readiness. A configured but source-less continuous mount is not an empty playable stream. HLS requires registration and publication; after restart its old assets are absent until repopulated.

Listener receives 403 Forbidden

Check both [access] and the requested mount's [mounts.access]: a mount allow cannot bypass global restrictions. Verify the resolved client IP/trusted proxy chain, case-sensitive UA mode/value and any nonempty country allow requirement. A localhost/private client fails country allow even when IP allow matches. The public error deliberately hides the reason; inspect sanitized management policy fields, the three access-denial counters and opt-in debug reason logs. HEAD follows the same ACL decision. During drain, continuous GET returns 503 before access evaluation.

Access policy or GeoIP rejects startup

Check the policy field named in the startup error, IP/CIDR syntax, UA mode and 1–256-byte value, ISO alpha-2 codes and per-list bounds. Mount country restrictions need the shared global database path; a mount cannot declare its own. Resolve relative paths beside the TOML file and verify readability, MMDB integrity/country schema and fixed database validation limits. A configured unusable database rejects startup even with no active country restrictions. Quikcast does not silently disable the policy or download a replacement.

Listener is refused or disconnects

Check drain, connection/global/per-mount limits, missing source/readiness, slow-consumer ring eviction, socket write stalls and source EOF. A smaller ring can shorten available catch-up history; increasing it is not a substitute for resolving a required-client correctness issue. Native inspection/disconnect reasons help distinguish the paths.

Relay keeps retrying

Inspect /api/relays/MOUNT for state, sanitized error, upstream status and next retry. Check endpoint/credentials, format, certificate and timeout contract. Stop it administratively to suppress retry. Restart re-applies static enabled; editing enabled in the file without restarting does nothing.

Browser HLS fails while FFmpeg works

Check player support, exact CORS origin, public TLS certificate/route and actual Range requests. Range is rejected by the current contract. Also confirm playlist advancement, producer authentication/admission and declared media data. A finalized FFmpeg fixture alone does not establish live browser compatibility.

Logs or startup fail with stdout redirection

Use a terminal or actively draining collector, not a regular-file daemon stdout. Do not rely on loss-free logging under a stopped collector. Inspect the dropped-record metric and collector lifecycle independently.

Fixed limits and settings that do not exist

The schema contains every operator-settable key documented above. Some bounds are fixed implementation contracts rather than configuration knobs:

  • Inbound initial request parser: 16 KiB, 64 header fields.

  • Continuous transport chunk size: 16 KiB.

  • Station metadata aggregate: 8 KiB; unsupported fields are not mirrored arbitrarily.

  • Local MP3/AAC ICY interval: 16,000 audio bytes, track title at most 1,024 bytes.

  • Shared Ogg page bound: 65,307 bytes, initialization: 65,536 bytes. Ogg FLAC has its own fixed mapping count/packet limits; native FLAC keys do not alter them.

  • Listener IP/UA allow and deny lists: 256 entries each, independently per global/mount policy; country lists: 249 entries each, counting input before deduplication.

  • UA rule values: 1–256 bytes, case-sensitive exact/prefix/substring only.

  • Shared country MMDB: 128 MiB, at most 2 million network records during country-schema validation; loaded once before binding.

  • Two retained continuous generations per configured mount.

  • Relay outbound headers: 16 KiB / 64 fields; ICY remote block at most 4,080 bytes, remote metadata interval 1–16,777,216.

  • Relay DNS/driver/address-count bounds, hostname/certificate validation and response framing rules: relay contract.

  • HLS description/upload/retention rules: typed HLS contract.

No configuration keys currently implement encoder subprocesses, AutoDJ, continuous-to-HLS conversion, station databases/history, arbitrary new codecs, source takeover/priorities, source/management ACLs, per-HLS access tables, regex UA rules, ASN/city filtering, automatic GeoIP updates, dynamic mount creation, runtime reload, native inbound TLS, custom relay CA/client certificates, redirects, PROXY protocol, HLS Range support, fMP4/LL-HLS/encryption/disk persistence, or a web administration UI.

Source station/track metadata, HLS rendition descriptions, runtime stop/drain commands, TLS proxy configuration and OS/service-manager limits live in their respective producer/control/deployment interfaces. Do not put invented equivalents into TOML: unknown fields reject startup.

Configuration documentation does not change feature acceptance or validate production capacity. Review actual clients/deployment and current build evidence separately using the manual validation checklist.

08 October 2026