Quikcast Help

Listener access control

Listener access is optional, static configuration evaluated before continuous listener admission. A request must pass the global policy and the requested mount's policy. Explicit denies win over allows; each nonempty allow list requires a positive match. Empty or omitted lists impose no restriction. Omitting [access] and [mounts.access] preserves existing listener behavior.

Rejected public requests receive HTTP 403 Forbidden, with a generic Forbidden response. HEAD has an empty response body. Clients cannot discover the rule, category, country or lookup state from this response.

Complete examples

A shared country database with additional restrictions on /radio:

listen = "127.0.0.1:8000" admin_listen = "127.0.0.1:9090" # Enable only when this peer is your trusted proxy: # trusted_proxies = ["127.0.0.1/32"] [access] geoip_database = "/etc/quikcast/GeoLite2-Country.mmdb" ip_deny = ["203.0.113.0/24", "2001:db8:dead::/48"] user_agent_deny = ["substring:python-requests", "prefix:curl/"] [[mounts]] path = "/radio" secret_file = "secrets/radio-source" [mounts.access] countries_allow = ["GH", "NG"]

A LAN mount without any GeoIP dependency:

listen = "127.0.0.1:8000" admin_listen = "127.0.0.1:9090" [[mounts]] path = "/lan" secret_file = "secrets/lan-source" [mounts.access] ip_allow = ["10.0.0.0/8", "192.168.0.0/16", "fd00::/8"] user_agent_allow = ["exact:station-player", "prefix:RadioPlayer/"]

Create the referenced source secret files according to the configuration guide. All six list fields are accepted globally and per continuous mount: ip_allow, ip_deny, user_agent_allow, user_agent_deny, countries_allow, countries_deny. Only the global [access] table accepts geoip_database. Relative database paths resolve beside the TOML configuration. A mount cannot disable or override global restrictions.

IP and trusted proxies

Rules accept IPv4/IPv6 addresses and CIDRs, for example 203.0.113.8, 203.0.113.0/24, 2001:db8::1, and 2001:db8::/32. Single addresses match exactly. CIDR host bits are masked when parsed. Invalid addresses and prefix lengths reject startup; there are no hostname rules or reverse DNS calls.

IP policy and GeoIP both use the existing canonical resolved client IP. Quikcast walks validated X-Forwarded-For hops from an explicitly trusted socket peer, stopping at the first untrusted hop. Forwarded headers from an untrusted peer are ignored. Do not trust arbitrary networks; trusted-proxy configuration is security-sensitive and IP restrictions depend on correct client resolution.

IPv4-mapped IPv6 peers and forwarded hops are canonicalized to IPv4, so ::ffff:192.0.2.1 matches 192.0.2.0/24. Mapped rules with prefixes 96–128 normalize to IPv4 prefixes 0–32. Mapped rules with shorter prefixes are rejected as ambiguous. Ordinary IPv6 rules apply to IPv6 identities; canonicalized mapped identities use IPv4 rules.

User-Agent

Rules use explicit case-sensitive matching, without regex or locale transformations:

  • exact:bad-monitor matches the entire value.

  • prefix:curl/ matches the beginning.

  • substring:python-requests matches anywhere.

Modes and values are parsed once at startup. Values must contain 1–256 bytes and no control characters. A missing or non-text User-Agent fails any nonempty UA allow list; deny-only UA policy permits absence. Deny wins when allow and deny both match. Matching uses the full already-parsed header within the existing 16 KiB HTTP header ceiling, without allocating a copy. Inspection retains its existing 256-character truncation.

User-Agent is client-controlled and spoofable. These rules provide compatibility and access restriction, not an authenticated identity.

Country database and lookup behavior

Install an operator-provided MaxMind-compatible country MMDB and configure its path. Quikcast does not download, license, update, watch or refresh databases. The operator owns installation and replacement; a replacement takes effect after restart. IP and UA rules work without a database.

Any nonempty country list, including mount-only country policy, requires the shared database. Country configuration accepts actual ISO 3166-1 alpha-2 codes, normalizes them to uppercase and deduplicates them. Invalid codes reject startup.

The database is opened once before binding, loaded into an immutable maxminddb::Reader<Vec<u8>>, verified for structural integrity, and checked for valid country.iso_code records. Unreadable, corrupt, incompatible or country-less databases reject startup even if no country list is active. No per-request filesystem or network I/O occurs. One lazy lookup is shared by global and mount policy. The lookup uses country.iso_code; it does not substitute registered country, continent, ASN or city information.

Successful lookup denies a country in the deny list and requires membership in any nonempty allow list. An address absent from the database, or a record without a country, is unknown. Private, loopback, link-local, unspecified, multicast, IPv4 broadcast/zero-network/shared-address-space and IPv6 unique-local addresses are classified as local without lookup. Both unknown and local results fail country allow lists and pass deny-only country policy. IP rules still apply normally, with no implicit private-address bypass.

A genuine runtime lookup/decoding failure is separately classified and fails closed whenever country policy is active, including deny-only policy. It still returns the same public 403. Database validation minimizes this possibility.

GeoIP is approximate and depends on database accuracy and freshness. Country restrictions are not legal/compliance guarantees or identity authentication.

Ordering, fallback, lifecycle and HLS

Global policy runs first, then requested mount policy. Within each policy: IP deny, IP allow, country deny/allow, UA deny, UA allow. An earlier IP rejection skips GeoIP. A global rejection stops evaluation. Access checks occur before fallback metadata/media resolution, ring joins, listener records and global/serving-mount permits.

A request to /main passes global and /main policy, then may receive /backup media through fallback. The internal serving step does not apply /backup policy. A direct /backup request applies its own policy. Requested identity owns access policy; serving identity owns media and resource limits. Fallback admissions and listener accounting remain unchanged for admitted GET requests.

GET and HEAD evaluate the same policy. HEAD returns existing serving/fallback metadata without a listener lease, permit or admission count. Drain keeps its existing 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 listeners retain existing drain/shutdown behavior.

Global listener policy applies to public HLS GET/HEAD playback, before playlist/segment serving. Per-continuous-mount restrictions never apply to HLS. Existing HLS playback remains available during drain when global policy passes. HLS OPTIONS/CORS handling remains unchanged. This adds no HLS-specific policy model.

Source ingest, /admin/metadata, native management, internal HLS producers and internal relay connections retain their existing authentication and trust models. ACLs do not replace management authentication.

Bounds and visibility

Each IP and UA allow/deny list is limited to 256 entries, independently for global and each mount policy. Each country allow/deny list is limited to 249 entries. Bounds count input entries before deduplication. Typed duplicate networks (including differing CIDR host bits), UA rules and normalized country codes are deduplicated. Overlapping networks remain separate; there is no CIDR-merging optimizer.

The existing configuration file ceiling is 128 KiB. The shared MMDB is limited to 128 MiB and country-schema validation to 2 million network records. These limits are intended for country databases, not general-purpose firewall datasets.

Authenticated /api/server inspection includes the listener-access-control capability, sanitized global access information, geoip_loaded, and mount_access_enabled. Mount inspection includes sanitized access information: counts of IP and UA allow/deny rules and normalized country lists. IP/UA rule contents and database filesystem paths are omitted. Mount inspection shows its own restrictions; global inspection shows the cumulative prerequisite.

Three fixed counters track denial categories: listener_access_denied_ip_total, listener_access_denied_geo_total, listener_access_denied_user_agent_total. They have no user-controlled labels. Rejections do not increment listener admissions, fallback admissions, active-listener counters or capacity/authentication rejection counters. Existing connection and response-byte accounting still operates normally.

Typed reasons appear in debug-level structured rejection logs with the bounded requested mount. No IP, UA or country value is logged by this feature, and successful decisions are not logged. Debug detail is opt-in and uses the existing bounded nonblocking logging sink.

Changes require restart. There are no ACL mutation endpoints, hot reload, external lookup calls, regex rules, source ACLs or source takeover in this milestone.

Validation evidence

The milestone adds offline evaluator/startup tests and real HTTP tests for IP/UA/country composition, trusted proxies, fallback identity/accounting, drain precedence, HEAD, and public HLS playlist/segment serving. The synthetic MMDB fixture is committed under fixtures/geoip with its MIT license; production databases are operator-owned.

Local manual validation on 2026-10-08 used disposable loopback instances and curl. Thirteen probes checked localhost deny and HEAD, unrestricted audio streaming, blocked/allowed UA values, allowed GB/denied JP/unknown/private fixture addresses, trusted proxy resolution, deny precedence, mapped forwarded IPv4, and ignored forwarding from an untrusted peer. Every expected status matched; each allowed GET received audio. Allowed continuous streams were deliberately stopped with curl's short timeout, so exit 28 is expected for those probes. Raw records are retained locally in evidence/listener-access/manual.json; automated output is in evidence/listener-access/tests.log.

08 October 2026