<?xml version="1.0" encoding="utf-8"?>
<!-- name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.miek.nl" -->
<rfc version="3" ipr="trust200902" docName="draft-prf-protocol-01" submissionType="IETF" category="info" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" indexInclude="true">

<front>
<title abbrev="PRF Protocol">The PRF Protocol</title><seriesInfo value="draft-prf-protocol-01" stream="IETF" status="informational" name="Internet-Draft"></seriesInfo>
<author initials="V." surname="QwQ" fullname="Venti QwQ"><organization></organization><address><postal><street></street>
</postal><email>huzpsb@qq.com</email>
</address></author><date year="2026" month="May" day="10"></date>
<area>Applications</area>
<workgroup>Independent Submission</workgroup>
<keyword>prf</keyword>
<keyword>tcp-forwarding</keyword>
<keyword>relay</keyword>

<abstract>
<t>This document describes the PRF protocol (&quot;Prf is Reversed Frp&quot;), a lightweight,
binary-length-prefixed TCP relay messaging protocol. PRF enables peer-to-peer
tunneling of TCP connections through a public middle relay node, originally
designed for Minecraft multiplayer sessions behind NAT or firewall environments.
The protocol defines three roles - Server, Client, and Middle - and a set of
four wireframe message types for room registration, connection requests, worker
handoff, and direct Minecraft client handshake parsing.</t>
</abstract>

</front>

<middle>

<section anchor="introduction"><name>Introduction</name>
<t>PRF is a custom application-layer protocol that operates over raw TCP
sockets. It is designed to facilitate TCP port forwarding between peers
that cannot directly connect to each other due to network constraints,
such as Network Address Translation (NAT) or firewall restrictions. A
publicly accessible middle relay node brokers connections between a
Server (which hosts a service) and a Client (which wishes to access that
service).</t>
<t>The protocol does not provide encryption, authentication, or integrity
protection. It is intended for use in trusted or low-risk environments
where the primary concern is connectivity rather than confidentiality.</t>

<section anchor="terminology"><name>Terminology</name>
<t>The key words &quot;MUST&quot;, &quot;MUST NOT&quot;, &quot;REQUIRED&quot;, &quot;SHALL&quot;, &quot;SHALL NOT&quot;,
&quot;SHOULD&quot;, &quot;SHOULD NOT&quot;, &quot;RECOMMENDED&quot;, &quot;NOT RECOMMENDED&quot;, &quot;MAY&quot;, and
&quot;OPTIONAL&quot; in this document are to be interpreted as described in BCP
14 <xref target="RFC2119"></xref> <xref target="RFC8174"></xref> when, and only when, they appear in all
capitals, as shown here.</t>
</section>

<section anchor="roles"><name>Roles</name>
<t>The PRF protocol defines three distinct roles:</t>

<dl spacing="compact">
<dt><strong>Server (Mode 1):</strong></dt>
<dd>The party that hosts the actual service (e.g., a Minecraft server).
It registers a room identifier with the Middle node and waits for
incoming Client connections to be forwarded.</dd>
<dt><strong>Client (Mode 2):</strong></dt>
<dd>The party that wishes to access the Server's service. It sends a
connection request to the Middle node specifying a room identifier.</dd>
<dt><strong>Middle (Mode 3):</strong></dt>
<dd>A publicly accessible relay node that brokers connections between
Servers and Clients. It maintains a mapping of room identifiers to
registered Servers and pending Client connections.</dd>
</dl>
</section>
</section>

<section anchor="protocol-overview"><name>Protocol Overview</name>
<t>PRF is a request-response protocol in which all communication flows
through the Middle node. The Server registers with the Middle and then
polls for pending Client connections. The Client sends a connection
request to the Middle. When a Client is matched with a Server, the
Middle assigns a Client Worker Identifier (CW-ID) and notifies the
Server. The Server then initiates a worker connection back to the
Middle to claim the Client, after which a bidirectional TCP tunnel is
established.</t>
<t>The protocol also defines a null-type mode in which no type identifier is
sent. In this mode, the Middle node inspects the raw initial bytes of the
TCP stream to determine routing. One recognized payload format in this mode
is the Minecraft client handshake packet, making the protocol compatible
with native Minecraft clients without requiring the PRF Client tool.</t>
</section>

<section anchor="wire-format"><name>Wire Format</name>

<section anchor="byte-order"><name>Byte Order</name>
<t>All multi-byte numeric fields are transmitted in network byte order
(big-endian).</t>
</section>

<section anchor="primitive-types"><name>Primitive Types</name>

<section anchor="integer-int32"><name>Integer (int32)</name>
<t>A 32-bit signed integer encoded in 4 bytes, big-endian. The maximum
value is 2,147,483,647.</t>
</section>

<section anchor="string"><name>String</name>
<t>A string is encoded as a length-prefixed sequence of UTF-8 bytes:</t>

<artwork><![CDATA[ 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Length (int32)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Data (UTF-8 bytes) ...                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
]]></artwork>
<t>The Length field specifies the number of UTF-8 bytes that follow.</t>
</section>

<section anchor="byte-array-bytes"><name>Byte Array (bytes)</name>
<t>A byte array is encoded identically to a String, with a 4-byte length
prefix followed by the raw bytes. Unlike String, the payload of a
byte array is not assumed to be valid UTF-8.</t>
</section>
</section>
</section>

<section anchor="message-types"><name>Message Types</name>
<t>Every message sent to the Middle node begins with a type identifier
field, which is a length-prefixed string containing one of the
following four values. The type identifier determines how the
remainder of the message is parsed.</t>

<section anchor="type-s-server-registration"><name>Type &quot;S&quot; - Server Registration</name>

<section anchor="purpose"><name>Purpose</name>
<t>Sent by the Server to register a room identifier with the Middle node.</t>
</section>

<section anchor="format"><name>Format</name>

<artwork><![CDATA[ 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Type Length (= 1)                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Type ("S")   |               Room ID Length                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               |        Room ID (UTF-8 bytes) ...              |
~                                                               ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
]]></artwork>
<t>The Type field is the UTF-8 string <tt>&quot;S&quot;</tt>. The Room ID is a UTF-8
string of at most 30 bytes identifying the room.</t>
</section>

<section anchor="response"><name>Response</name>
<t>Upon receiving a Type &quot;S&quot; message, the Middle node sends a response
string.</t>
<t><strong>Success vs. Failure:</strong> The first character of the response string
determines the outcome. A response beginning with <tt>&gt;</tt> (U+003E) indicates
success; any other response indicates failure. The remainder of the
string is a server-defined, internationalized (i18n) message that MAY
be displayed to the user but MUST NOT be used for protocol-level
decision making.</t>
</section>

<section anchor="reconnection"><name>Reconnection</name>
<t>If the Room ID ends with the character <tt>v</tt> (U+0076, case-insensitive)
and the room ID is already registered, the Middle node evicts the
existing Server registration and replaces it with the new connection.
The evicted Server receives the string <tt>&quot;EXIT&quot;</tt> (without a leading
type identifier) on its poll channel, indicating that it should
terminate.</t>
</section>

<section anchor="polling"><name>Polling</name>
<t>After successful registration, the Server enters a polling loop. The
Middle node sends a String (without a leading type identifier) to the
Server at intervals of up to 5 seconds:</t>

<ul spacing="compact">
<li>An empty string (<tt>&quot;&quot;</tt>) serves as a keepalive signal; the Server
takes no action.</li>
<li>A non-empty string is a CW-ID (Client Worker Identifier) of the
form <tt>CW&lt;n&gt;</tt>, where <tt>&lt;n&gt;</tt> is a decimal integer. The Server MUST
spawn a streamer to handle this Client connection by sending a
Type &quot;W&quot; message.</li>
</ul>
</section>
</section>

<section anchor="type-c-client-connection-request"><name>Type &quot;C&quot; - Client Connection Request</name>

<section anchor="purpose-1"><name>Purpose</name>
<t>Sent by the Client to request a connection to a room identified by its
Room ID.</t>
</section>

<section anchor="format-1"><name>Format</name>

<artwork><![CDATA[ 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Type Length (= 1)                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Type ("C")   |      Room ID (String, max 30)                 |
~                                                               ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
]]></artwork>
<t>The Type field is the UTF-8 string <tt>&quot;C&quot;</tt>. The Room ID is the
identifier of the target room, at most 30 bytes.</t>
</section>

<section anchor="response-1"><name>Response</name>
<t>Upon receiving a Type &quot;C&quot; message, the Middle node sends a response
string.</t>
<t><strong>Success vs. Failure:</strong> The first character of the response string
determines the outcome. A response beginning with <tt>&gt;</tt> (U+003E) indicates
success; any other response indicates failure. The remainder of the
string is a server-defined, internationalized (i18n) message that MAY
be displayed to the user but MUST NOT be used for protocol-level
decision making.</t>
<t>If the response indicates success, the Client SHOULD proceed to
establish a bidirectional tunnel.</t>
</section>
</section>

<section anchor="type-w-worker-handoff"><name>Type &quot;W&quot; - Worker Handoff</name>

<section anchor="purpose-2"><name>Purpose</name>
<t>Sent by the Server's streamer to claim a pending Client connection.</t>
</section>

<section anchor="format-2"><name>Format</name>

<artwork><![CDATA[ 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Type Length (= 1)                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Type ("W")   |      CW-ID (String, max 30)                   |
~                                                               ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
]]></artwork>
<t>The Type field is the UTF-8 string <tt>&quot;W&quot;</tt>. The CW-ID field is the
Client Worker Identifier previously received from the Middle during
polling.</t>
</section>

<section anchor="response-2"><name>Response</name>
<t>Upon receiving a Type &quot;W&quot; message, the Middle node sends a response
string.</t>
<t><strong>Success vs. Failure:</strong> The first character of the response string
determines the outcome. A response beginning with <tt>&gt;</tt> (U+003E) indicates
success; any other response indicates failure. The remainder of the
string is a server-defined, internationalized (i18n) message that MAY
be displayed to the user but MUST NOT be used for protocol-level
decision making.</t>
<t>On success, a bidirectional tunnel is established between this
streamer's socket and the corresponding Client's socket. The response
message MAY contain the IP address of the connected Client as
informational text.</t>
</section>

<section anchor="haproxy-proxy-protocol-header"><name>HAProxy PROXY Protocol Header</name>
<t>If the room identifier starts with the character <tt>V</tt> (U+0056,
case-insensitive), the Middle node prepends a HAProxy PROXY protocol
version 1 header <xref target="PROXY"></xref> to the Client-to-Server direction of the
tunnel before bridging. The header has the following format:</t>

<artwork><![CDATA[PROXY TCP4 <client-ip> 192.0.2.1 10000 10000\r\n
]]></artwork>
<t>Where <tt>&lt;client-ip&gt;</tt> is the actual IP address of the Client. The
destination address and port are placeholders (192.0.2.1:10000). This
mechanism allows the target service to obtain the real Client IP
address.</t>
</section>
</section>

<section anchor="type-null-untyped-raw-stream-inspection"><name>Type null - Untyped Raw Stream Inspection</name>

<section anchor="purpose-3"><name>Purpose</name>
<t>When a connection arrives at the Middle node without a leading type
identifier string, the Middle enters an untyped raw stream inspection
mode. Rather than rejecting the connection, the Middle reads the
initial bytes from the TCP stream and attempts to identify a
recognized payload format. If a format is recognized, the Middle
extracts routing information (the room identifier) and bridges the
connection; if not, the connection is terminated with an error.</t>
<t>This is not a general-purpose passthrough - it is a protocol-defined
extension point that allows PRF to interoperate with existing
application protocols without requiring a PRF-specific client. The
specific format described below is the Minecraft client handshake,
which PRF happens to be compatible with by design.</t>
</section>

<section anchor="recognized-format-minecraft-client-handshake"><name>Recognized Format: Minecraft Client Handshake</name>
<t>The Minecraft client handshake packet is one payload format
recognized in the null-type mode. When the Middle identifies this
format, it extracts a room identifier from the Server Name
Indication (SNI) hostname carried in the packet.</t>
<t>The packet format is defined by the Minecraft protocol. Within PRF,
the relevant fields are:</t>

<artwork><![CDATA[ 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                 Total Packet Length (VarInt)                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Packet ID = 0x00 (Byte)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   Protocol Version (VarInt)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                 Server Name Length (VarInt)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                 Server Name (UTF-8 bytes) ...                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Port (Unsigned Short)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Next State (VarInt)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
]]></artwork>
<t>The Total Packet Length MUST be at least 10 bytes and at most 100 bytes. The
Packet ID MUST be 0x00 (Handshake). The Server Name Length MUST be
at least 5.</t>
</section>

<section anchor="room-id-extraction"><name>Room ID Extraction</name>
<t>The Middle node extracts the subdomain from the Server Name (SNI)
hostname by taking all characters before the first <tt>.</tt> (U+002E)
character. For example, if the SNI is <tt>myroom.relay.example.com</tt>,
the extracted room ID is <tt>myroom</tt>.</t>
<t>The full handshake packet is buffered and forwarded unmodified to the
target Server after the tunnel is established.</t>
</section>

<section anchor="failure-handling"><name>Failure Handling</name>
<t>If the room ID cannot be found, the Middle node sends a Minecraft
Disconnect/Login packet (Packet ID 0x00) to the Client with a JSON
reason string in the format:</t>

<artwork><![CDATA[{"text":"<reason>"}
]]></artwork>
<t>This terminates the Client connection with an appropriate error
message displayed in the Minecraft client.</t>
</section>
</section>
</section>

<section anchor="protocol-constants"><name>Protocol Constants</name>
<t>The following constants are defined by the PRF protocol:</t>
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>

<tbody>
<tr>
<td>Maximum Room ID Length</td>
<td>30 bytes</td>
<td>Maximum UTF-8 byte length of a room/server identifier</td>
</tr>

<tr>
<td>Maximum Response Length</td>
<td>100 bytes</td>
<td>Maximum UTF-8 byte length of a Middle response string</td>
</tr>

<tr>
<td>Minimum MC Packet Length</td>
<td>10 bytes</td>
<td>Minimum Minecraft handshake packet total length</td>
</tr>

<tr>
<td>Maximum MC Packet Length</td>
<td>100 bytes</td>
<td>Maximum Minecraft handshake packet total length</td>
</tr>

<tr>
<td>Minimum Server Name Length</td>
<td>5 bytes</td>
<td>Minimum SNI hostname length</td>
</tr>

<tr>
<td>Maximum VarInt Bytes</td>
<td>5</td>
<td>Maximum bytes for a single VarInt encoding</td>
</tr>

<tr>
<td>Poll Interval</td>
<td>5 seconds</td>
<td>Interval at which the Server polls for new CW-IDs</td>
</tr>

<tr>
<td>Initial Read Timeout</td>
<td>5 seconds</td>
<td>Middle socket read timeout for type detection</td>
</tr>

<tr>
<td>Reconnect Delay</td>
<td>10 seconds</td>
<td>Delay before a Server reconnects after disconnect</td>
</tr>

<tr>
<td>Streamer Bridge Delay</td>
<td>10 seconds</td>
<td>Delay before a MiddleStreamer begins bridging data</td>
</tr>

<tr>
<td>CW-ID Prefix</td>
<td>&quot;CW&quot;</td>
<td>Prefix for Client Worker Identifiers</td>
</tr>

<tr>
<td>PROXY destination address</td>
<td>192.0.2.1</td>
<td>Placeholder destination IP in PROXY header</td>
</tr>

<tr>
<td>PROXY destination port</td>
<td>10000</td>
<td>Placeholder destination port in PROXY header</td>
</tr>
</tbody>
</table></section>

<section anchor="protocol-flow"><name>Protocol Flow</name>

<section anchor="server-registration-and-client-connection"><name>Server Registration and Client Connection</name>
<t>The following diagram illustrates the normal protocol flow for a
Server registering and a Client connecting:</t>

<artwork><![CDATA[Server                         Middle                        Client
  |                              |                              |
  |--- "S" + roomId ------------>|                              |
  |<-- ">success" ---------------|                              |
  |                              |                              |
  |                              |<-- "C" + roomId -------------|
  |                              |--- ">success" -------------->|
  |                              |                              |
  |<-- "CW0" (poll) -------------|                              |
  |--- "W" + "CW0" ------------->|                              |
  |<-- ">success (<ip>)" ---------|                             |
  |                              |                              |
  |<================== Bidirectional Tunnel ===================>|
  |       (raw TCP relay)        |       (raw TCP relay)        |
]]></artwork>
</section>

<section anchor="untyped-raw-stream-inspection-e-g-minecraft-client"><name>Untyped Raw Stream Inspection (e.g., Minecraft Client)</name>

<artwork><![CDATA[Native Client                  Middle                         Server
  |                              |                              |
  |--- MC Handshake ------------>|                              |
  |    (SNI: myroom.relay.example.com)  |                       |
  |                              |--- extracts roomId="myroom"  |
  |                              |--- assigns CW-ID             |
  |                              |                              |
  |                              |--- "CW0" (poll) ------------>|
  |                              |<-- "W" + "CW0" --------------|
  |                              |                              |
  |<================== Bidirectional Tunnel ===================>|
  |    (handshake forwarded)     |    (handshake forwarded)     |
]]></artwork>
</section>
</section>

<section anchor="security-considerations"><name>Security Considerations</name>
<t>The PRF protocol as defined in this document does not provide any
form of encryption, authentication, or integrity protection. All
data, including room identifiers, is transmitted in cleartext over
TCP. The following security implications should be considered:</t>

<ul spacing="compact">
<li><strong>Traffic Interception:</strong> Any party on the network path can read
all tunneled data, including the room identifier and any
application-layer content.</li>
<li><strong>Traffic Injection:</strong> Without integrity protection, an attacker
can inject or modify packets in transit.</li>
<li><strong>Room ID Enumeration:</strong> Room identifiers are transmitted in
cleartext and can be observed or guessed by an attacker, allowing
unauthorized connection to a Server.</li>
<li><strong>No Server Authentication:</strong> Clients have no cryptographic means
to verify that they are connecting to the intended Server or
through a legitimate Middle node.</li>
<li><strong>No Client Authentication:</strong> Servers accept connections from any
Client that provides the correct room identifier.</li>
</ul>
<t>PRF is designed to provide no additional security guarantees beyond
those offered by the underlying transport. The protocol itself does
not perform encryption, authentication, content inspection, or access
control. Consequently, the overall security of a PRF-tunneled connection
depends entirely on the security properties of the application-layer
protocol carried within the tunnel. Implementations and deployments
SHOULD ensure that the tunneled protocol provides its own encryption
and authentication if confidentiality and integrity are required.</t>
</section>

<section anchor="iana-considerations"><name>IANA Considerations</name>
<t>This document has no IANA actions.</t>
<t>--- back</t>
</section>

<section anchor="acknowledgements"><name>Acknowledgements</name>
<t>The PRF protocol name is a playful backronym: &quot;Prf is Reversed Frp,&quot;
referencing the frp (Fast Reverse Proxy) tool that inspired its
design.</t>
</section>

</middle>

<back>
<references><name>Normative References</name>
<reference anchor="PROXY" target="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">
  <front>
    <title>HAProxy Proxy Protocol</title>
    <author>
      <organization>HAProxy Technologies</organization>
    </author>
    <date year="2011"></date>
  </front>
</reference>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.2119.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8174.xml"/>
</references>

</back>

</rfc>
