rl-c-client

Test responder contract

r_test_responder is a deterministic UDP fixture for testing applications that embed rl-c-client. It is test support, not a Ratelimitly server and not part of the production C library API.

The responder lives in this repository so packet encoding, authentication, and protocol changes remain next to their source of truth. Downstream tests invoke the versioned executable; they must not include private headers from src/ or reimplement Ratelimitly packets.

Build and start

Build the fixture explicitly:

make test-responder

Start one deterministic scenario on an explicit address and port:

./bin/r_test_responder \
  --listen=127.0.0.1:39080 \
  --scenario=allow

--listen is process control for the test fixture, not a Ratelimitly server command-line option or a protocol-level address restriction. A production Ratelimitly server does not expose this fixture flag; endpoint selection belongs to the server deployment and runtime. The fixture is instead launched directly by a test harness, so the harness must supply its numeric IPv4 or bracketed IPv6 bind address and nonzero UDP port, then direct its DNS/SRV setup and client to a reachable endpoint. This avoids a hidden endpoint or port default and lets tests choose their own network topology.

All explicitly supplied numeric bind addresses, including wildcard addresses, are valid. The example and automated suite choose 127.0.0.1 only to keep routine test traffic on the same host; that choice states no requirement about a Ratelimitly server. The responder must not open a socket unless --listen is present.

On startup it writes exactly one readiness record to standard output:

{"event":"ready","address":"127.0.0.1","port":39080,"server_id":1,"auth":"aes"}

All subsequent standard-output records are newline-delimited JSON. Diagnostics go to standard error. On POSIX, SIGINT and SIGTERM cause a clean socket close and exit status zero. Win32 handles console Ctrl-C and Ctrl-Break cooperatively through SetConsoleCtrlHandler. Windows may terminate console processes directly during close, logoff, or system shutdown, so those events are not documented as graceful fixture exits.

The fixture builds on POSIX and native Win32. POSIX waits with poll(2); Windows uses WinSock select, preserves pointer-width SOCKET values, and is initialized and cleaned up with WSAStartup/WSACleanup. The Win32 example’s CMake project builds r-test-responder.exe with the selected compiler so MSVC tests exercise this same protocol implementation.

Synthetic credentials

The responder contains only clearly marked synthetic AES and cookie fixtures. The default is the repository AES fixture. --auth=aes and --auth=cookie select between those built-in credentials; arbitrary credentials are not accepted on the command line.

--print-nginx-config prints the matching synthetic tenant/auth directives, SRV target, and responder address without opening a socket. --listen is still required — its address is rendered into the generated configuration rather than bound:

bin/r_test_responder --listen=127.0.0.1:39082 --print-nginx-config

This output is for generated test configurations only and must label the credential as synthetic.

The server id defaults to 1 and may be replaced with --server-id=<n> so a local DNS fixture can advertise the matching s-<n>.localhost target. The credential tenant key id and response server id are distinct protocol fields.

Scenarios

Exactly one base scenario is selected per process:

Scenario Required behavior
allow Return every requested guard and resource in request order with passing values.
deny Return every requested item, with a nonzero deficit on each resource.
guard-pass Pass every guard and allow every resource.
guard-deny Set each guard current latency to its threshold and allow resources.
quota Allow the first --allow-count=<n> rate requests per bucket, then return a deficit.
drop Authenticate and observe packets but send no response.
malformed-auth Respond with authentication material that the client must reject.
malformed-truncated Send a deliberately truncated authenticated response.
malformed-request-id Send an otherwise valid response with a different request id.
count-empty Send a valid authenticated success response with zero guards and resources.
count-short Return one fewer result than requested, preferring a missing resource.
count-extra Return one additional result beyond the request counts.

Client-observable outcomes, in terms of the public C API: allow, guard-pass, and count-empty complete the request callback with RCLIENT_OK and success == true; deny, guard-deny, and an exhausted quota complete with RCLIENT_OK and success == false (inspect deficits and guard results); count-short and count-extra complete with RCLIENT_OK and result arrays whose counts differ from the request — match entries by ID, not index. drop produces no response, so the request completes with RCLIENT_ERR_TIMEOUT. malformed-auth and malformed-truncated make r_client_on_datagram return an informational error (RCLIENT_ERR_AUTH for a cookie mismatch, RCLIENT_ERR_PROTOCOL for an AES tag or parse failure) while the request stays in flight and later times out. malformed-request-id is ignored silently (RCLIENT_OK ingress return) and the request times out.

--delay-ms=<n> delays a non-drop response without blocking signal handling. --steering=rebind sets steering feedback so the client asks its host to change the source port; the default is --steering=keep.

--max-packets=<n> exits successfully after observing the requested number of authenticated packets. A zero value means run until signaled.

Observations

The responder emits one record for each authenticated input packet. A rate request record contains at least:

{"event":"rate_request","sequence":1,"guards":1,"resources":2,"label":"api","tracker":{"ttl_ms":30000,"max_samples":100,"buffer_size":100,"min_sample_threshold":1},"guard_threshold_ms":100,"disposition":"guard-pass"}

A latency report record includes the first report’s tracker configuration, observed value, and whether its service identity matches the preceding guard:

{"event":"latency_report","sequence":2,"reports":1,"tracker":{"ttl_ms":30000,"max_samples":100,"buffer_size":100,"min_sample_threshold":1},"observed_latency_ms":25,"matches_previous_guard":true}

The runtime event stream must never print credential material, raw authenticated packets, bucket ids, or service ids. The explicit --print-nginx-config mode is the only credential-output exception and labels the credential as synthetic. Tracker parameters and the identity-match flag let downstream tests validate rate/latency pairing without exposing the service identifier itself.

Malformed or unauthenticated inputs produce an input_rejected record and no response. Invalid command-line configuration exits nonzero before writing a readiness record.

Determinism and state

These properties let downstream suites restart the responder between cases and obtain the same result without a private service, wall-clock rate windows, or network access.

Release versioning

The test responder is versioned with rl-c-client. Its command line and JSONL records describe that tagged release only. During the MVP, a later release may change them directly without a compatibility layer. Breaking fixture changes must still be called out in release notes so downstream test suites can update their dependency lock deliberately.