Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ Resolvers can be combined with any adapter. Implementing the `Resolver` interfac

Adapters 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.

## PROXY protocol

When the DNS server sits behind a proxy or load balancer that speaks the [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) (AWS NLB, HAProxy, nginx, Envoy, etc.), enable it on the `Server` so resolver callbacks see the real client address instead of the proxy's. Both v1 (text) and v2 (binary) headers are parsed, and the feature applies to both UDP datagrams and TCP connections.

```php
$server = new Server($adapter, $resolver);
$server->setProxyProtocol(enabled: true);
$server->start();
```

Detection is per-connection (TCP) or per-datagram (UDP): traffic that begins with a PROXY v1/v2 signature is parsed and the real client address is passed to the resolver; traffic without a signature is handled as a direct DNS request. This keeps health checks and direct clients working during rollouts.

> [!WARNING]
> Only enable PROXY protocol on listeners that are exclusively reachable by trusted proxies. An untrusted client can forge a PROXY header to spoof its source address. Bind to an internal network interface or enforce network-level ACLs.

## DNS client

The bundled client can query any DNS server and returns fully decoded messages.
Expand Down
50 changes: 42 additions & 8 deletions src/DNS/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,67 @@

namespace Utopia\DNS;

/**
* Transport adapter contract.
*
* Adapters own the wire — UDP sockets, TCP sockets, connection framing,
* and PROXY protocol preamble handling. From the {@see Server}'s
* perspective, DNS messages appear as generic "request with a client
* address"; the adapter hides everything below.
*/
abstract class Adapter
{
/**
* Worker start
* Whether incoming traffic may be prefixed with a PROXY protocol
* preamble. When enabled, the adapter strips the preamble (if
* present) and reports the real client address through the
* {@see onMessage()} callback.
*/
protected bool $enableProxyProtocol = false;

public function setProxyProtocol(bool $enabled): void
{
$this->enableProxyProtocol = $enabled;
}

public function hasProxyProtocol(): bool
{
return $this->enableProxyProtocol;
}

/**
* Worker start callback. Invoked once per worker.
*
* @param callable(int $workerId): void $callback
* @phpstan-param callable(int $workerId): void $callback
*/
abstract public function onWorkerStart(callable $callback): void;

/**
* Packet handler
* Register the DNS message handler.
*
* Invoked once per complete DNS message, regardless of transport.
* The adapter has already stripped any PROXY preamble and extracted
* the framed message; $ip/$port reflect the real client address.
*
* The callback returns the response bytes to send back to the
* client, or an empty string to suppress the response.
*
* $maxResponseSize is the maximum response size appropriate for the
* transport (UDP: 512 per RFC 1035 unless EDNS0; TCP: 65535).
*
* @param callable(string $buffer, string $ip, int $port, ?int $maxResponseSize): string $callback
* @phpstan-param callable(string $buffer, string $ip, int $port, ?int $maxResponseSize):string $callback
* @phpstan-param callable(string $buffer, string $ip, int $port, ?int $maxResponseSize): string $callback
*/
abstract public function onPacket(callable $callback): void;
abstract public function onMessage(callable $callback): void;

/**
* Start the DNS server
* Start the DNS server.
*/
abstract public function start(): void;

/**
* Get the name of the adapter
*
* @return string
* Get the name of the adapter.
*/
abstract public function getName(): string;
}
69 changes: 69 additions & 0 deletions src/DNS/Adapter/Composite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Utopia\DNS\Adapter;

use Utopia\DNS\Adapter;

/**
* Fans `Server` hooks out to multiple transport adapters.
*
* Useful for common setups like "UDP + TCP on the same port", which
* typically involves a shared underlying runtime (e.g. one
* `Swoole\Server` with both a primary port and an added TCP listener).
* Each underlying adapter is assumed to be non-blocking during
* wiring — one of them owns the blocking event loop and is started
* last; the others are expected to no-op their `start()`.
*/
class Composite extends Adapter
{
/** @var list<Adapter> */
protected array $adapters;

public function __construct(Adapter ...$adapters)
{
$this->adapters = array_values($adapters);
}

public function setProxyProtocol(bool $enabled): void
{
parent::setProxyProtocol($enabled);
foreach ($this->adapters as $adapter) {
$adapter->setProxyProtocol($enabled);
}
}

public function onWorkerStart(callable $callback): void
{
foreach ($this->adapters as $adapter) {
$adapter->onWorkerStart($callback);
}
}

public function onMessage(callable $callback): void
{
foreach ($this->adapters as $adapter) {
$adapter->onMessage($callback);
}
}

public function start(): void
{
foreach ($this->adapters as $adapter) {
$adapter->start();
}
}

public function getName(): string
{
$names = array_map(fn (Adapter $a) => $a->getName(), $this->adapters);
return 'composite(' . implode('+', $names) . ')';
}

/**
* @return list<Adapter>
*/
public function getAdapters(): array
{
return $this->adapters;
}
}
Loading
Loading