Architecture¶
The Device Register service sits between the filtering Recursor (or
dnsdist in front of it) and the Redis / Postgres / Kafka backends
that the rest of Platform Filter reads from. It turns a stream of
UDP "new device" notifications into registered devices the portal UI
can see and the Recursors can stop nagging about.
Role in Platform Filter¶
The upstream producer is the filtering Recursor: every DNS query that comes from an unknown device triggers a short UDP register message to this service. The downstream consumers are:
- Redis — authoritative for "which user owns this device", and the place the Recursor converges on via its local LMDB sync. Once a device lands in Redis, the Recursor stops emitting register messages for it.
- Postgres — the table behind the portal UI. Populated by this
service unless
kafka_onlyis on (see below). - Kafka — an optional alternative to Postgres. In
kafka_onlymode an external system is expected to own the Postgres write, and this service only publishes to Kafka and Redis. dstore_alert— optional protobuf-over-TCP fan-out for downstream alerting.
The service does not talk to the Recursor or the portal directly. The only contract between them is the data layout in Redis and the schema of the Postgres table the admin portal manages.
Combined mode flow¶
In combined mode, one process owns the whole pipeline end to end:
flowchart LR
R[Recursor / dnsdist] -- UDP --> U[UDP receiver]
U --> M[Deduplication & rate limiting]
M --> D[Postgres or Kafka]
D --> X[Redis write<br/>(atomic Lua script)]
X --> P[(device_profile<br/>pub/sub)]
D -. read .-> UI[Portal UI]
X -. LMDB sync .-> R
The Postgres (or Kafka) write happens before the Redis write: the
service looks up the user's numeric profile in Postgres at the
same time as it inserts the device row, and then passes the profile
through to the Redis write so the stored mapping carries it. If
kafka_only is on, the Kafka publish must succeed before the device
is added to Redis; otherwise nothing goes in.
The Redis write itself is performed atomically by an embedded Lua
script that updates the device_profile hash and publishes a change
notification on the device_profile pub/sub channel. This is an
implementation detail — the script is not configurable and operators
do not touch it.
Split mode flow¶
In split mode the same pipeline is broken into two services that talk to each other over NATS:
flowchart LR
R[Recursor / dnsdist] -- UDP --> P[Producer instance]
P --> PM[Local dedup &<br/>rate limiting]
PM -- JSON --> N((NATS))
N --> C[Consumer instance]
C --> CM[Global rate limiting]
CM --> D[Postgres or Kafka]
D --> X[Redis write]
X -. LMDB sync .-> R
D -. read .-> UI[Portal UI]
- The producer runs next to the Recursor. It opens the UDP
listener, does a first round of deduplication and rate limiting on
the traffic it sees locally, and publishes each accepted device to
NATS. It does not touch Redis, Postgres, Kafka, or
dstore_alert. A per-instance rate limit (see--nats-producer-rate-limit) caps how many messages it will forward per second, so a compromised or runaway producer cannot saturate the NATS transport. - The consumer runs on the Control Plane next to the backends. It does not listen on UDP at all; it subscribes to the NATS subject, runs a second round of rate limiting and deduplication across the aggregated stream from all producers, and then writes to the backends exactly like combined mode does.
Two rate-limit layers are intentional. The producer-side limit is a local safeguard, and the consumer-side limit is what actually enforces the global per-user device quota, because only the consumer sees the full stream.
Core concepts¶
These terms are used throughout the configuration and operations pages.
Device¶
A device is identified by a (username, deviceid) pair. The device
id is opaque to the service — typically a MAC-like hex identifier
the Recursor pulled out of the DNS query. The service may also carry
an optional DHCP name and a numeric profile id alongside the pair.
User / subscriber¶
Rate limiting and blocking are per-user, not per-device. A user who registers too many devices in too short a window trips the blocklist for a cool-off period.
Profile¶
A numeric profile id looked up from Postgres (from the
storage_device table) and stored alongside each device in Redis.
Because the profile lookup happens on the database pipeline, disabling
the database pipeline also disables the Redis write path — the Redis
writer has nothing to pass through.
Deduplication¶
The service filters duplicate register messages for the same device within a short window. This absorbs bursts of traffic when many Recursors notice the same new device simultaneously, and it means a restarting Recursor re-sending all of its "unknown device" cache at once does not hammer the database.
Blocklist¶
A user who sees more than block-max-devices distinct registrations
in a rolling window of 2 × block-time-window is blocked from
further registration for block-time. Two details matter:
- The window is doubled. The service keeps two rolling counter
buckets of
block-time-windowseconds each, and blocks when the sum exceeds the threshold. With the defaults (block-time-window: 10s,block-max-devices: 10) a user registering 11+ devices in any 20-second span trips the blocklist. - DNS still works for blocked users. The blocklist only suppresses registration. The user's devices keep resolving DNS normally; they just do not appear in the portal UI for the duration of the block.
Storage backends¶
Redis¶
The authoritative store that the Recursors converge on. Every accepted
device ends up in a Redis hash called device_profile, keyed by
username^deviceid, and a notification is published on the
device_profile pub/sub channel so downstream subscribers can react
in real time. The write is performed by an embedded Lua script that
runs server-side inside Redis, which keeps it atomic under concurrent
updates.
Postgres¶
The service inserts each new device into the table the portal UI
reads, and consults the same database to look up the user's profile
id at the same time. Connection details, worker pool size, and the
libpq connection string all live in the
database: section.
Kafka (and kafka_only mode)¶
Kafka is an alternative to Postgres for the write side. With
kafka_only: true, the service publishes each device to the
configured Kafka topic and expects an external system to take care
of the Postgres row. In that mode the device is only added to Redis
after the Kafka publish succeeded — if Kafka rejects the message,
the device is not added to Redis either, so the Recursor will re-send
the notification later.
Kafka can also be enabled alongside Postgres (with kafka_only: false),
in which case the service writes to both.
dstore_alert¶
Optional. If dstore.alert-address
is set, the service opens a persistent TCP connection to that address
and forwards a protobuf-encoded alert for each new device. This path
is entirely optional — leaving the address empty disables it.
Why the split mode exists¶
The original combined mode does not work well across data-centre boundaries, and the split mode was added to address three concrete problems:
- Dynamic IPs. The Lua platform settings need a stable IP to identify the Recursor sending the register message, which is awkward in deployments where infrastructure hands out IPs dynamically on every restart.
- Encrypted cross-DC traffic. Customers prefer to avoid unencrypted traffic between data-centre boundaries, and the combined mode has no authenticated transport to offer for the cross-boundary leg.
- UDP is hard to proxy. Load balancers and ingress proxies are built around TCP; UDP requires dedicated plumbing and often separate VIPs.
Running a split-producer next to the Recursor keeps the UDP on a local network segment, and turns the cross-boundary leg into authenticated NATS traffic that a standard TCP load balancer can handle.