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 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.
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.
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.
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.
quota counters start at zero for every process and are not persisted.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.
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.