Skip to content

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_only is on (see below).
  • Kafka — an optional alternative to Postgres. In kafka_only mode 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/>&#40;atomic Lua script&#41;]
    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 &#38;<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-window seconds 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.