Important
This repository is a read-only mirror of the utopia-php monorepo. Development happens in packages/dns — please open issues and pull requests there.
Utopia DNS is a modern PHP 8.3 toolkit for building DNS servers and clients. It provides a fully-typed DNS message encoder/decoder, pluggable resolvers, and telemetry hooks so you can stand up custom authoritative or proxy DNS services with minimal effort.
Although part of the Utopia Framework family, the library is framework-agnostic and can be used in any PHP project.
composer require utopia-php/dnsThe library requires PHP 8.5+ with the ext-sockets extension. The Swoole adapter additionally needs the ext-swoole extension.
Create an authoritative DNS server by wiring an adapter (which transports to listen on) and a resolver (how records are answered). The example below uses the native PHP socket adapter with UDP and TCP transports and the in-memory zone resolver.
<?php
require __DIR__ . '/vendor/autoload.php';
use Utopia\DNS\Adapter\Native;
use Utopia\DNS\Message\Record;
use Utopia\DNS\Resolver\Memory;
use Utopia\DNS\Server;
use Utopia\DNS\Zone;
$adapter = new Native([
new Native\Udp('0.0.0.0', 5300),
new Native\Tcp('0.0.0.0', 5300),
]);
$zone = new Zone(
name: 'example.test',
records: [
new Record(name: 'example.test', type: Record::TYPE_A, rdata: '192.0.2.1', ttl: 60),
new Record(name: 'www.example.test', type: Record::TYPE_CNAME, rdata: 'example.test', ttl: 60),
new Record(
name: 'example.test',
type: Record::TYPE_SOA,
rdata: 'ns1.example.test hostmaster.example.test 1 7200 1800 1209600 3600',
ttl: 60
),
]
);
$server = new Server($adapter, new Memory($zone));
$server->setDebug(true);
$server->start();The server listens on UDP and TCP port 5300 (RFC 5966) and answers queries for example.test from the in-memory zone. Implement the Utopia\DNS\Resolver interface to serve records from databases, APIs, or other stores.
Memory: authoritative resolver backed by aZoneobjectProxy: forwards queries to another DNS serverCloudflare: proxy resolver preconfigured for1.1.1.1/1.0.0.1Google: proxy resolver preconfigured for8.8.8.8/8.8.4.4
Resolvers can be combined with any adapter. Implementing the Resolver interface allows you to plug in custom logic while reusing the message encoding/decoding and telemetry tooling.
Resolvers receive a Query, which carries the decoded Message plus its source: the client IP, port, and transport protocol. Cross-cutting concerns such as tracing or rate limiting compose as resolver decorators:
final readonly class RateLimited implements Resolver
{
public function __construct(private Resolver $inner) {}
public function resolve(Query $query): Message
{
if ($this->isFlooding($query->ip)) {
return Message::response($query->message->header, Message::RCODE_REFUSED, authoritative: false);
}
return $this->inner->resolve($query);
}
}Note that UDP source addresses can be forged; stream transports (Protocol::Tcp, Protocol::Https) carry verified peer addresses.
An adapter composes one or more transports into a single process. Transports are responsible only for receiving and returning raw packets — they call back into the server with the payload, source IP, and port so your resolver logic stays isolated.
Native: blocking select loop based onext-sockets, withNative\UdpandNative\TcptransportsSwoole: non-blocking server built on the Swoole runtime, withSwoole\Udp,Swoole\Tcp, andSwoole\Httptransports
Transports of one adapter share the process and (for Swoole) the worker pool. UDP and TCP transports can bind the same port number; each HTTP transport needs its own TCP port:
use Utopia\DNS\Adapter\Swoole;
$adapter = new Swoole(
transports: [
new Swoole\Udp('0.0.0.0', 53),
new Swoole\Tcp('0.0.0.0', 53),
new Swoole\Http('0.0.0.0', 443, certPath: '/etc/ssl/dns.crt', keyPath: '/etc/ssl/dns.key'),
],
workers: 4,
);Swoole\Http implements DNS over HTTPS (RFC 8484): it accepts wire-format queries via GET (?dns= base64url) and POST (application/dns-message). Leave certPath unset to serve plain HTTP behind a TLS-terminating proxy.
Load balancers usually replace the client address with their own. The TCP transports accept a PROXY protocol v1/v2 header (as sent by DigitalOcean, AWS, or HAProxy load balancers) when constructed with proxyProtocol: true, and report the original client address to your resolver:
new Swoole\Tcp('0.0.0.0', 53, proxyProtocol: true);
new Native\Tcp('0.0.0.0', 53, proxyProtocol: true);Only enable this behind a load balancer that sends the header — with it enabled, direct DNS clients are rejected. The HTTP transport uses new Swoole\Http('0.0.0.0', 443, trustProxy: true) instead, which reads the X-Forwarded-For header. PROXY protocol is a TCP feature: UDP transports always see the packet source address, so preserve it at the network layer (for example externalTrafficPolicy: Local on Kubernetes).
The bundled client can query any DNS server and returns fully decoded messages.
<?php
require __DIR__ . '/vendor/autoload.php';
use Utopia\DNS\Client;
use Utopia\DNS\Message;
use Utopia\DNS\Message\Question;
use Utopia\DNS\Message\Record;
$client = new Client('1.1.1.1');
$query = Message::query(
new Question('example.com', Record::TYPE_A)
);
$response = $client->query($query);
foreach ($response->answers as $answer) {
echo "{$answer->name} {$answer->ttl} {$answer->getTypeName()} {$answer->rdata}\n";
}Server::setTelemetry() accepts any adapter from utopia-php/telemetry. Counters (dns.queries.total, dns.responses.total) and a histogram (dns.query.duration) are emitted automatically, enabling Prometheus or OpenTelemetry pipelines with minimal configuration.
- Install dependencies:
composer install - Static analysis:
composer check - Coding standards:
composer lint(usecomposer formatto auto-fix) - Tests:
composer test - Sample Swoole server for manual testing:
docker compose up
Run composer bench to start a sample Swoole server and measure UDP, TCP, and DNS over HTTPS throughput against it with dnspyre (fetched automatically when not installed; tune with the PORT, QUERIES, CONCURRENCY, and DOMAINS environment variables). To benchmark an already-running server directly:
dnspyre -n 250 -c 20 --server 127.0.0.1:5300 dev.appwrite.ioThe report covers requests per second, success counts, and latency percentiles per transport.
Version 2.0 replaces per-adapter protocol flags with composable transports:
new Native($host, $port)→new Native([new Native\Udp($host, $port), new Native\Tcp($host, $port)])new Swoole($host, $port, $numWorkers)→new Swoole([new Swoole\Udp($host, $port), new Swoole\Tcp($host, $port)], workers: $numWorkers)enableTcp: false→ omit theTcptransport- TCP tuning options (
maxTcpClients,maxTcpBufferSize,maxTcpFrameSize,tcpIdleTimeout) moved to theNative\Tcpconstructor asmaxClients,maxBufferSize,maxFrameSize, andidleTimeout Resolver::resolve()now receives aUtopia\DNS\Querycarrying the decoded message and its source ($query->message,$query->ip,$query->port,$query->protocol) instead of a bareMessageAdapter::getName()andResolver::getName()were removed without replacement- The server no longer emits
utopia-php/spantraces (dns.packet); metrics viaServer::setTelemetry()are unchanged. Emit spans from yourResolverimplementation if you need tracing
MIT