swsn / react-proxy-protocol
PROXY protocol v1/v2 decoder/encoder and ReactPHP TCP/UDP socket helpers
Package info
codeberg.org/stadtwerke-schwerin/react-proxy-protocol
pkg:composer/swsn/react-proxy-protocol
Requires
- php: ^8.4
- evenement/evenement: ^3.0
- react/datagram: ^1.10
- react/event-loop: ^1
- react/promise: ^3
- react/socket: ^1.16
- react/stream: ^1.4
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-02 20:12:02 UTC
README
PROXY protocol v1/v2 for ReactPHP — inbound listener and outbound encoder / write helpers.
| Namespace | SWSN\ProxyProtocol\ |
| PHP | ^8.4 |
| License | MIT |
TCP (inbound)
Wraps a ServerInterface, strips the PROXY header, then re-emits a
React\Socket\Connection so SecureServer / STARTTLS still work.
The header is read from the non-blocking raw socket via MSG_PEEK (same idea as
React’s StreamEncryption): only the header bytes are consumed. Application
payload / TLS ClientHello stay in the kernel — no full-stream buffer, no
per-chunk copy after detach. Declared v2 lengths above
ProxyProtocolDecoder::MAX_HEADER_BYTES (4096) are rejected early.
resumeAfterHeader only controls whether the connection is resumed after the
header (it does not select a different decode strategy):
true (default) | Plaintext apps — resume so data flows |
false | Next layer is SecureServer — stay paused |
Plaintext
$tcp = new React\Socket\SocketServer('0.0.0.0:25');
$server = new SWSN\ProxyProtocol\ProxyProtocolServer(
$tcp,
trustedCidrs: ['10.90.0.0/24'],
required: true,
);
Implicit TLS
SecureServer requires the internal Connection class and must own the read
watcher for stream_socket_enable_crypto. Keep the connection paused:
$tcp = new React\Socket\SocketServer('0.0.0.0:465');
$behindProxy = new SWSN\ProxyProtocol\ProxyProtocolServer(
$tcp,
trustedCidrs: ['10.90.0.0/24'],
required: true,
resumeAfterHeader: false,
);
$tls = new React\Socket\SecureServer($behindProxy, null, [
'local_cert' => '/certs/server.pem',
]);
Stack: TcpServer → ProxyProtocolServer → SecureServer.
getRemoteAddress() on the emitted connection is the PROXY source
(tcp://client:port, tls://… after handshake).
UDP (inbound)
Wraps a React\Datagram\Socket. Per the PROXY protocol spec,
header and payload MUST be in the same datagram; each datagram is decoded
independently (typically v2 with family 0x12 / 0x22).
$factory = new React\Datagram\Factory();
$udp = await($factory->createServer('0.0.0.0:162'));
$server = new SWSN\ProxyProtocol\ProxyProtocolDatagramServer(
$udp,
trustedCidrs: ['10.90.0.0/24'],
required: true,
);
$server->on('message', function (
string $payload,
string $address,
SWSN\ProxyProtocol\ProxyProtocolDatagramServer $socket,
string $proxyPeer,
): void {
// $address = client from PROXY header (host:port)
// $proxyPeer = actual UDP peer (load balancer) — use for replies via LB
});
Outgoing
ProxyProtocolEncoder builds v1/v2 headers. TCP: send the header once at
the start of the connection. UDP: prepend the header on every datagram.
TCP connector (header once)
$encoder = new SWSN\ProxyProtocol\ProxyProtocolEncoder();
$header = $encoder->encodeV2(
SWSN\ProxyProtocol\ProxyProtocolV2Family::TcpOverIpv4,
'203.0.113.10',
56424,
'10.0.0.1',
25,
);
$connector = new SWSN\ProxyProtocol\ProxyProtocolConnector(
new React\Socket\Connector(),
$header, // or fn (string $uri): string => …
);
$connector->connect('127.0.0.1:25')->then(function ($conn) {
$conn->write("EHLO\r\n");
});
TCP writable wrap (header once)
$out = new SWSN\ProxyProtocol\ProxyProtocolWritableStream($connection, $header);
$out->write('payload'); // sends header + payload, further writes are plain
TLS outbound
Same order as inbound: PROXY on cleartext, then TLS. Put
ProxyProtocolConnector inside SecureConnector (not the other way around):
$encoder = new SWSN\ProxyProtocol\ProxyProtocolEncoder();
$header = $encoder->encodeV2(
SWSN\ProxyProtocol\ProxyProtocolV2Family::TcpOverIpv4,
'203.0.113.10',
56424,
'10.0.0.1',
465,
);
// TcpConnector → PROXY header once → TLS handshake → app writes
$tcp = new React\Socket\Connector(); // or TcpConnector
$behindProxy = new SWSN\ProxyProtocol\ProxyProtocolConnector($tcp, $header);
$tls = new React\Socket\SecureConnector($behindProxy, null, [
'verify_peer' => true,
'peer_name' => 'mail.example.com',
]);
$tls->connect('tls://mail.example.com:465')->then(function (
React\Socket\ConnectionInterface $connection,
): void {
// connection is encrypted; PROXY header was already sent on cleartext
$connection->write("EHLO mail.example.com\r\n");
});
Stack: Connector → ProxyProtocolConnector → SecureConnector.
Alternatively, write the header yourself on a paused cleartext connection, then
StreamEncryption::enable($connection) — same wire order.
UDP (header every packet)
$datagram = $encoder->wrap($header, $payload);
$socket->send($datagram, $remote);
// or when answering via ProxyProtocolDatagramServer:
$server->sendProxied($payload, $header, $proxyPeer);
Testing
composer install
vendor/bin/phpunit