--- v: 3 title: PCP Hostname Extension for SNI-Demultiplexed Port Mappings abbrev: PCP Hostname Extension docname: draft-start9-pcp-hostname-00 category: std submissiontype: IETF consensus: true ipr: trust200902 area: Internet workgroup: Network Working Group keyword: - PCP - SNI - NAT - port mapping - QUIC date: 2026-06-23 author: - ins: A. McClelland name: Aiden McClelland org: Start9 email: me@drbonez.dev normative: RFC6887: RFC6066: RFC5890: RFC8999: RFC9000: RFC9001: RFC7753: informative: RFC7652: I-D.ietf-tls-esni: I-D.ietf-quic-load-balancers: --- abstract This document defines a Port Control Protocol (PCP) option, HOSTNAME, that associates one or more fully qualified domain names with a MAP request. A PCP server implementing this option demultiplexes inbound connections on a shared external port by inspecting the Server Name Indication presented in the TLS ClientHello and forwards each connection to the internal host that holds the binding for the presented name. This allows multiple internal hosts behind a single external IP address to share well-known ports (e.g. 443) provided their hostnames differ, while each host retains the ability to self-provision its mappings via ordinary PCP semantics. Demultiplexing is defined for TCP-carried TLS, with optional support for QUIC. --- middle # Introduction {#intro} ## Motivation {#motivation} A NAT gateway exposes one external IP address, or at most a few. A conventional PCP MAP grants an entire (protocol, external IP, external port) tuple to a single internal host, so the number of hosts that can serve HTTPS on port 443 is limited to the number of external addresses, typically one. Operators work around this with a manually configured reverse proxy or SNI proxy on the gateway, which: 1. requires central, out-of-band configuration rather than client self-provisioning, and 2. typically replaces the client source address with the proxy address, hiding the real peer from the backend. Since hosts behind a NAT typically serve public Internet domains (the names are registered in public DNS and resolve to the gateway's external address), the hostname is a natural demultiplexing key that the internal host already knows and can assert itself. This extension moves the SNI routing table into PCP, so each internal host registers its own names, and the gateway, which is already on the return path for all LAN traffic, forwards with the original client source address preserved. ## Requirements Language {#conventions} {::boilerplate bcp14-tagged} ## Terminology {#terminology} This document uses the terminology of {{RFC6887}} (PCP client, PCP server, internal host, external address, mapping, mapping nonce). In addition: Hostname binding: : an association, held by the PCP server, from (protocol, external IP address, external port, hostname) to an internal address and internal port, created by a MAP request carrying a HOSTNAME option. Fallback mapping: : a conventional mapping (created by a MAP request with no HOSTNAME option) on a port that also has hostname bindings; it receives connections that match no hostname binding (see {{validity}}). Demultiplexed port: : a (protocol, external IP address, external port) tuple holding at least one hostname binding. Inbound connections to a demultiplexed port are routed per {{tcp-demux}} or {{quic-demux}} rather than by conventional destination NAT. # Overview of Operation {#overview} This section is informative; normative requirements appear in {{hostname-option}} through {{security}}. 1. An internal host sends a PCP MAP request for protocol TCP, suggesting an external port (e.g. 443), and includes one or more HOSTNAME options, each carrying an FQDN such as `git.example.com`. 2. The PCP server checks each requested name against its hostname bindings for that (external IP, external port). If no conflicting binding exists, it creates bindings with the requested lifetime and returns success, echoing the HOSTNAME options. 3. Other internal hosts may map the same external port with different hostnames; the port is shared because the mapping key is extended to (protocol, external IP, external port, hostname). 4. For each inbound TCP connection to a demultiplexed port, the server accepts the connection, reads the TLS ClientHello, extracts the server_name extension, selects the matching binding, and splices the connection to the bound internal host. 5. Because the PCP server is the default gateway for the internal network, it originates the internal leg with the original client's source address and port (transparent mode, {{source-pres}}); return packets necessarily traverse the gateway, which steers them back into the spliced connection. A host requests forwards for multiple hostnames either by including multiple HOSTNAME options in a single MAP request or by issuing multiple MAP requests; see {{client}}. # The HOSTNAME Option {#hostname-option} ## Format {#format} The option follows the PCP option format ({{Section 7.3 of RFC6887}}): ``` 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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Option Code | Reserved | Option Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | : hostname (variable length) : | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` {: #fig-format title="HOSTNAME Option Format"} Option Code: : TBD1, in the optional-to-process range. Option Length: : length of the hostname field in octets (1 to 255). The option data is zero-padded to a 32-bit boundary per {{RFC6887}}; padding is not included in Option Length. hostname: : the name to bind, encoded exactly as it appears on the wire in the SNI HostName field ({{Section 3 of RFC6066}}): ASCII, internationalized names as A-labels {{RFC5890}}, no trailing dot, no NUL octets, total length 1 to 255 octets. Literal IPv4 and IPv6 addresses MUST NOT be used ({{Section 3 of RFC6066}}). Comparison is case-insensitive; servers SHOULD normalize names to lowercase on receipt. A syntactically invalid hostname MUST be rejected with MALFORMED_OPTION. Because the option code is in the optional-to-process range, a server that does not implement this option ignores it and processes the request as a conventional MAP, granting an unrestricted port forward. This degrades gracefully: if the requesting host is the only one using the port, observable behavior is identical, and a second host receives CANNOT_PROVIDE_EXTERNAL (or an alternate port) per standard MAP processing. Clients detect whether hostname scoping was applied by checking for the echoed HOSTNAME option(s) in the response; see {{client}}. ## Validity and the Fallback Mapping {#validity} The HOSTNAME option is valid only for the MAP opcode. A server implementing this option MUST reject any other opcode carrying it with MALFORMED_OPTION. The option is valid for protocol TCP, and for protocol UDP only on servers implementing QUIC demultiplexing ({{quic-demux}}). A server implementing this option MUST reject a HOSTNAME option for any other protocol, or for UDP when QUIC demultiplexing is not implemented, with UNSUPP_HOSTNAME ({{result-codes}}). Clients MUST NOT request hostname bindings for protocols whose servers send application data before the client (e.g. SMTP, IMAP); a server cannot detect this misuse, and such connections will stall waiting for a ClientHello and then be handled as in {{tcp-demux}}, step 4. A HOSTNAME option MAY appear multiple times in a single MAP request, each occurrence creating one binding, subject to the atomic failure semantics of {{conflicts}}. There is no in-option fallback syntax. A conventional MAP request (no HOSTNAME option) for the same (protocol, external IP, external port) creates the fallback mapping: connections whose ClientHello carries no server_name extension, presents a name with no matching binding, or whose initial bytes do not parse as TLS (or as QUIC per {{quic-demux}}), are forwarded to its holder. At most one fallback mapping exists per (protocol, external IP, external port), enforced by normal MAP exclusivity. ## Wildcards {#wildcards} A hostname whose first label is `*` (e.g. `*.example.com`) requests a wildcard binding matching exactly one left-most label, with the same semantics as certificate wildcard matching. Wildcard support is OPTIONAL; servers not implementing it respond with UNSUPP_HOSTNAME ({{result-codes}}). An exact binding takes precedence over a wildcard binding. # Server Behavior {#server} ## Mapping Table {#mapping-table} The server maintains, per (protocol, external IP, external port), a set of hostname bindings, each comprising: hostname, internal address, internal port, lifetime, and the mapping nonce of the creating client. All standard MAP processing of {{RFC6887}} applies (lifetime decay, renewal by matching nonce, ANNOUNCE/epoch recovery). ## Conflicts {#conflicts} If a requested hostname is already bound on that (protocol, external IP, external port) under a different mapping nonce, the server MUST NOT create or alter the binding and MUST return result code HOSTNAME_TAKEN ({{result-codes}}). If any HOSTNAME option in a request fails validation or conflicts, the entire request fails and no bindings from that request are created or modified; PCP responses are atomic per request. A conventional MAP for a (protocol, external IP, external port) that has hostname bindings is permitted and serves as the fallback mapping ({{validity}}). It is subject to ordinary MAP exclusivity. Hostname bindings and the fallback mapping have independent lifetimes; expiry of one does not affect the others. ## Deletion {#deletion} A MAP request with requested lifetime 0 carrying HOSTNAME options deletes only the named bindings, subject to the nonce-matching rules of {{Section 15 of RFC6887}}. A lifetime-0 request without HOSTNAME options deletes only the requester's conventional (fallback) mapping, if any. Other bindings on the port are unaffected in both cases. ## TCP Demultiplexing {#tcp-demux} For TCP connections arriving at a demultiplexed port, the server: 1. Completes the TCP handshake with the external client. 2. Reads until it has the full ClientHello or a parse failure, subject to a buffer cap (16384 octets, the maximum TLS plaintext record fragment, is RECOMMENDED) and a timeout (5 seconds is RECOMMENDED). ClientHellos can span multiple TCP segments and, rarely, multiple TLS records; servers SHOULD handle multi-record ClientHellos. 3. Extracts server_name, selects the binding (exact match, then wildcard, then the fallback mapping if present), and connects to the bound internal host and port, replaying the buffered bytes before splicing. 4. If no binding matches and no fallback mapping exists, the server SHOULD close the connection. Whether to reset or send a TLS alert is implementation defined; the server MUST NOT forward the connection to an arbitrary binding. Demultiplexing engages only on demultiplexed ports. A port holding only a conventional mapping is forwarded as plain destination NAT per {{RFC6887}}, with no ClientHello inspection. When the first hostname binding is added to a port with an existing conventional mapping, the port transitions to demultiplexed mode; established connections are unaffected, and the conventional mapping's holder begins receiving only fallback traffic for new connections. The reverse transition occurs when the last hostname binding expires. The server does not terminate TLS; it requires no keys and forwards the ClientHello bytes verbatim. ## QUIC Demultiplexing (OPTIONAL) {#quic-demux} A server MAY additionally implement demultiplexing of QUIC {{RFC9000}} for hostname bindings with protocol UDP. Servers implementing this section accept HOSTNAME options with protocol UDP; servers that do not MUST reject them per {{validity}}, so a client can discover support by the result code. For UDP datagrams arriving at a demultiplexed port: 1. Initial packets. A datagram beginning with a QUIC long header ({{RFC8999}}) of a version the server supports is processed as a candidate Initial. The server derives the Initial secrets from the client's Destination Connection ID and the version-specific salt ({{Section 5.2 of RFC9001}}), decrypts the packet, and reassembles the CRYPTO stream to obtain the TLS ClientHello. The ClientHello can span multiple Initial packets; the server MUST buffer and reassemble across datagrams, subject to a cap (10 datagrams or 65535 octets of CRYPTO data is RECOMMENDED) and a timeout (5 seconds is RECOMMENDED). 2. Binding selection. The server extracts server_name and selects a binding exactly as in {{tcp-demux}}, step 3. It then creates flow state keyed on the connection's 4-tuple, forwards the buffered datagrams verbatim to the bound internal host, and forwards subsequent datagrams matching the flow in both directions. No decryption is performed after binding selection, and the server never holds handshake or 1-RTT keys. 3. Flow lifetime. Flow state is removed after an idle period consistent with the gateway's UDP mapping timeout. Version Negotiation and Retry exchanges initiated by the internal host traverse the established flow and require no special handling. 4. Non-matching datagrams. Datagrams that are not parseable Initials of a supported version (including short-header packets matching no flow, long headers of unknown versions, and non-QUIC UDP payloads) are forwarded to the fallback mapping if one exists and otherwise dropped. Two consequences follow from routing on flow state. First, active connection migration and NAT rebinding both move the connection to a 4-tuple the server has no state for; such packets carry connection IDs opaque to the server and arrive as non-matching datagrams. Internal hosts serving QUIC behind a demultiplexing gateway SHOULD send the disable_active_migration transport parameter ({{Section 18.2 of RFC9000}}) to suppress client-initiated migration; this does not prevent NAT rebinding ({{Section 9.3 of RFC9000}}), which can still strand a connection on an unknown 4-tuple unless coordinated connection-ID routing is in use. Coordinated connection-ID routing (e.g. QUIC-LB {{I-D.ietf-quic-load-balancers}}) could lift this restriction and is out of scope. Second, a client whose first flight uses a QUIC version unknown to the server cannot be demultiplexed; servers SHOULD support all current standard versions and treat unknown-version long headers as non-matching rather than attempting Version Negotiation themselves. ## Source Address Preservation {#source-pres} Servers SHOULD originate the internal leg of demultiplexed TCP connections using the external client's source address and port (e.g. via IP_TRANSPARENT on Linux), relying on their position as the internal network's gateway to capture return traffic. When operating in this mode the forwarded connection is, from the internal host's perspective, indistinguishable from a direct destination-NAT forward. A server that cannot operate transparently uses its own address on the internal leg; this behavior is visible to internal hosts and operators should document it. QUIC demultiplexing ({{quic-demux}}) forwards datagrams at the IP layer and preserves the source address inherently. # Client Behavior {#client} - Clients include one HOSTNAME option per name. Given the atomic failure semantics of {{conflicts}}, clients SHOULD use one MAP request per name when independent success matters, and MAY batch multiple options in one request when the names succeed or fail together (e.g. a certificate SAN set). - Clients MUST examine the response for echoed HOSTNAME options. Absence means the server did not process the option and the granted mapping is unrestricted; clients that require hostname scoping MAY delete such a mapping. Against a server that ignores the option, both request patterns above degrade safely: a batched request yields one unrestricted mapping covering all names, and per-name requests for the same internal port resolve to that same mapping ({{RFC6887}} keys mappings on the internal tuple), acting as refreshes rather than conflicts. - On gateways with multiple external addresses, hostname bindings are scoped per external address: the same name may be bound on two external IPs to different internal hosts. Clients SHOULD set the Suggested External IP Address field ({{Section 11.1 of RFC6887}}) to the address their public DNS resolves to and SHOULD include PREFER_FAILURE so the server fails rather than assigning a different address. The Assigned External IP Address in the response is authoritative; clients publishing DNS records from PCP state SHOULD use it rather than the suggested address. - Clients renew exactly as in {{RFC6887}}, re-sending the same MAP request (same nonce, same options) before lifetime expiry. - Clients MUST be prepared to receive HOSTNAME_TAKEN at any renewal (e.g. after server state loss and a race with another host) and SHOULD surface the condition rather than silently retrying. # Interaction with Other PCP Options {#interactions} FILTER ({{RFC6887}}): : composes normally; filters restrict which remote peers may connect and are evaluated before demultiplexing. PREFER_FAILURE ({{RFC6887}}): : composes normally; see {{client}} for its use on multihomed gateways. THIRD_PARTY ({{RFC6887}}): : a request combining THIRD_PARTY and HOSTNAME options creates hostname bindings on behalf of the third party, subject to the server's THIRD_PARTY authorization policy; the conflict rules of {{conflicts}} apply to the third party's bindings identically. PORT_SET ({{RFC7753}}): : a request combining PORT_SET and HOSTNAME options is not meaningful (a hostname binding targets a single internal port) and MUST be rejected with MALFORMED_OPTION. # Interaction with Encrypted ClientHello {#ech} ECH {{I-D.ietf-tls-esni}} encrypts the true server name; the outer ClientHello carries the public name of the client-facing server. A demultiplexing server sees only the outer SNI, for both TLS over TCP and QUIC. Deployments using this extension with ECH MUST bind the public name (the name published in the HTTPS/SVCB ech parameter), and the internal host performs inner-name routing itself if needed. Names hidden by ECH cannot serve as demultiplexing keys by design; this is a property of ECH, not a defect of this extension. # New Result Codes {#result-codes} HOSTNAME_TAKEN (TBD2): : the requested hostname is already bound on this external IP address and port by another client. This is a short lifetime error; the condition clears when the conflicting binding expires or is deleted. UNSUPP_HOSTNAME (TBD3): : the hostname is syntactically valid but the request uses a feature this server does not support (e.g. wildcard bindings, or protocol UDP without QUIC demultiplexing support). This is a long lifetime error. Syntactically invalid hostnames are rejected with the existing MALFORMED_OPTION ({{RFC6887}}). # Security Considerations {#security} The security model is that of {{RFC6887}}: any host on the internal network may create mappings, so any host may claim any hostname, first come first served. This is equivalent in power to the existing ability of any internal host to claim an entire external port; the granularity is finer but the trust assumption is unchanged. Deployments requiring stronger guarantees SHOULD deploy PCP authentication {{RFC7652}} or restrict the option via policy (e.g. per-host hostname allowlists). A server MAY verify, at binding time, that the requested name resolves in public DNS to one of its external addresses, rejecting names that do not. DNS can change after binding and split-horizon deployments may legitimately fail such a check, so it MUST be possible to disable; it is a policy mechanism, not a protocol requirement. ClientHello collection ({{tcp-demux}} and {{quic-demux}}) creates state per pending connection. Servers MUST cap buffered bytes and the number of pending connections to bound memory under handshake-and-stall attacks, and SHOULD subject pending connections to the same limits as their existing connection tracking. The hostname presented in SNI is attacker controlled and unauthenticated. The server uses it only to select among pre-registered bindings; it MUST NOT be used to construct file paths or commands, and MUST be sanitized before inclusion in log output. For QUIC demultiplexing, an attacker can cheaply elicit Initial-secret derivation and decryption work with spoofed datagrams. The per-version key derivation uses no secret state and the cost is bounded per datagram, but servers SHOULD rate-limit Initial processing per source address. # Applicability and Limitations {#applicability} Demultiplexing requires a client-first protocol carrying SNI: TLS over TCP ({{tcp-demux}}) and QUIC ({{quic-demux}}). Non-TLS protocols with a cleartext hostname early in the stream (e.g. HTTP/1.1 Host) are out of scope; the fallback mapping covers them coarsely. Server-first protocols cannot be demultiplexed at all ({{validity}}). # IANA Considerations {#iana} ## PCP Option Code {#iana-option} IANA is requested to assign the following option code in the "PCP Options" registry of the "Port Control Protocol (PCP) Parameters" group, from the Specification Required range (192-223); values in this range are optional-to-process: | Field | Value | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Value | TBD1 | | Name | HOSTNAME | | Purpose | Associates a fully qualified domain name with a MAP request; inbound connections on the mapped external port are forwarded to the internal host whose binding matches the SNI presented in the TLS or QUIC ClientHello. | | Valid for Opcodes | MAP | | Length | variable; 1 to 255 octets | | May Appear in | Request. May appear in response only if it appeared in the associated request. | | Maximum Occurrences | As many as fit within maximum PCP message size. | | Reference | This document | ## PCP Result Codes {#iana-results} IANA is requested to assign two result codes in the "PCP Result Codes" registry of the same group, from the Specification Required range (128-191): | Value | Name | Description | Reference | | ----- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | | TBD2 | HOSTNAME_TAKEN | The requested hostname is already bound on the assigned external address and port by another client. This is a short lifetime error. | This document | | TBD3 | UNSUPP_HOSTNAME | The HOSTNAME option requests a feature not supported by this server. This is a long lifetime error. | This document | --- back # Implementation Status {#impl-status} An open-source implementation exists in StartOS and its StartTunnel gateway. Pending IANA assignment, it uses values from the PCP Private Use ranges: option code 224, and result codes 192 (HOSTNAME_TAKEN) and 193 (UNSUPP_HOSTNAME). These are placeholders and are expected to be replaced by the IANA-assigned values (TBD1, TBD2, TBD3) once allocated.