Repository navigation
fix: fall back to PROXY V2 SSL TLVs when the inner TLS has no peer cert - #235
Merged
Merged
Conversation
zmstone
force-pushed
the
261016-pp2-tlv-fallback-when-inner-not-mtls
branch
from
October 16, 2026 07:12
147fae0 to
c854f57
Compare
zmstone
marked this pull request as ready for review
October 16, 2026 07:14
A proxied connection to an SSL listener has two places a client certificate can come from, and which one holds it depends on where TLS was terminated. A proxy that terminates mTLS forwards the client's details in the PROXY V2 SSL TLVs. That hop is the one closest to the client, so the TLVs describe the client. The inner certificate, when the re-encrypted hop presents one at all, belongs to the proxy. A TCP-passthrough proxy terminates nothing, sends no SSL TLVs, and leaves the client certificate on the inner ssl_socket. PR #218 made peercert/1, peer_cert_subject/1 and peer_cert_common_name/1 delegate to the inner ssl_socket unconditionally. That fixed the passthrough deployment and broke the terminating one, which had worked before, because the TLV clause became unreachable. The break shows up as a version split: EMQX 5.8 and 5.10 pin esockd 5.13.0 and 5.14.0 and still read the TLVs, while 6.0 and later pin 5.17.x and read the inner socket, so a HAProxy deployment loses the client CN on upgrade to 6.x. Where the proxy also presents its own certificate on the inner hop, the current code does not fail at all: it reports the proxy as the client. Read the SSL TLVs first and fall back to the inner socket only when the header carries no certificate information. Enabling proxy_protocol on a listener already declares the upstream trusted; the peer address comes from the same header and is already used that way. No new option: the listener either trusts the header or does not enable the feature. One predicate, pp2_ssl_info/1, decides the source for all three accessors. Deciding per accessor would let peercert/1 report the proxy's certificate while peer_cert_common_name/1 reports the client's, because a proxy can send the SSL TLV without the CN sub-TLV. peersni/1 takes the same precedence for the same reason. The authority TLV is the name the client asked for; the inner SNI is whatever the proxy chose when it opened the re-encrypted hop. Tests cover the three deployments so none can be traded for another again: the existing t_peercert_proxy_ssl for a passthrough proxy with no TLVs, t_peercert_proxy_ssl_pp2_fallback for a terminating proxy with a one-way inner hop, and t_peercert_proxy_ssl_pp2_wins_over_inner for both sources populated. Negative controls: the second fails on the parent commit with {false,undefined,undefined}, the third fails against inner-first precedence.
zmstone
force-pushed
the
261016-pp2-tlv-fallback-when-inner-not-mtls
branch
from
October 16, 2026 07:56
c854f57 to
d083349
Compare
keynslug
approved these changes
Oct 16, 2026
2 of 3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A proxied connection to an SSL listener has two places a client certificate can
come from, and which one holds it depends on where TLS was terminated.
send-proxy-v2-ssl-cn) forwards theclient's details in the PROXY V2 SSL TLVs. The inner handshake is one-way, or
carries the proxy's own certificate.
tls: passthrough) terminates nothing. Itsends no SSL TLVs, and the client certificate is on the inner
ssl_socket.PR #218 made
peercert/1,peer_cert_subject/1andpeer_cert_common_name/1delegate to the inner
ssl_socketunconditionally when the listener is SSL. Thatfixed the passthrough deployment and broke the terminating one, which had worked
before, because the TLV clause became unreachable.
Deployments
tls: passthrough) + TLS listener, client mTLS to EMQXRow D does not fail loudly today. It reports HAProxy as every client.
Rows C and D are a 5.x to 6.x upgrade regression rather than a long-standing gap.
#218 first shipped in esockd 5.16.2, and the EMQX lines pin either side of it:
dev-58dev-510dev-60dev-63Change
Read the SSL TLVs first. Use the inner socket only when the header carries no
certificate information. Every row above then works, which neither predecessor
achieved.
The PROXY header comes from the hop closest to the client, so when it carries
certificate information it describes the client. An inner certificate on a
re-encrypted hop belongs to the proxy, not the client. Enabling
proxy_protocolon a listener already declares the upstream trusted — the peer address comes from
the same header and is already used that way — so this adds no new trust
assumption, and no new option: the listener either trusts the header or does not
enable the feature.
One predicate,
pp2_ssl_info/1, picks the source for all three accessors.Deciding per accessor would let
peercert/1report the proxy's certificate whilepeer_cert_common_name/1reports the client's, because a proxy can send the SSLTLV without the CN sub-TLV.
peersni/1takes the same precedence, for the same reason: the authority TLV isthe name the client asked for, the inner SNI is whatever the proxy chose when it
opened the re-encrypted hop.
Tests
Three cases pin rows A, C and D, so none can be traded for another again:
t_peercert_proxy_ssl/1(from fix: read peercert from inner ssl_socket on proxied SSL listeners #218) — row A. Passthrough proxy, PROXY V1, noTLVs. The inner socket is the only source. Unchanged and still passing.
t_peercert_proxy_ssl_pp2_fallback/1— row C. Terminating proxy, one-way innerTLS, PROXY V2 with a
PP2_SUBTYPE_SSL_CN. The TLVs are the only source.t_peercert_proxy_ssl_pp2_wins_over_inner/1— row D. Both populated: mTLS onthe inner hop and SSL TLVs in the header. Pins the precedence this PR chooses.
Negative controls: the row C case fails on
masterwith{false,undefined,undefined}; the row D case fails against inner-firstprecedence.
Full suite: 240 passed, 3 skipped (pre-existing, remote TCP test server absent).