Connection Limit Bypass via Original-Destination Listener Handoff
Affected commit: c33d01624d
Sink: source/common/listener_manager/active_tcp_socket.cc:229
Summary
Envoy checks a listener's connection limit only at initial accept. When listener filters restore an original destination, the socket is handed to the destination listener via onAcceptWorker() without re-checking the limit. Attackers can exceed the destination listener's connection cap.
Reproduce
#!/bin/bash
set -e
# Clone and identify HEAD
git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-repro
cd /tmp/envoy-repro
echo "HEAD: $(git rev-parse HEAD)"
# Verify onAcceptWorker is called without connection limit re-check
echo "--- Handoff to destination listener without limit check ---"
sed -n '220,237p' source/common/listener_manager/active_tcp_socket.cc
# Show that onAcceptWorker does not check connection limits
echo "--- onAcceptWorker signature (no limit parameter) ---"
grep -n 'onAcceptWorker' source/common/listener_manager/active_tcp_listener.cc | head -3
The vulnerable code at active_tcp_socket.cc:229:
new_listener.value().get().onAcceptWorker(std::move(socket_), false, false, network_namespace);
When a listener filter (such as envoy.filters.listener.original_dst) restores an original destination address, the socket is transferred to the matching destination listener via onAcceptWorker(). This method accepts the socket directly without re-evaluating the destination listener's per_connection_buffer_limit_bytes or listener_filters_timeout connection limits.
The initial listener checks its own connection limit during accept(), but after the original-destination handoff, the destination listener's limit is never consulted. An attacker routing many connections through the initial listener with varied original destinations can exceed the target listener's configured connection cap.
Expected output:
HEAD: <resolved-commit>
--- Handoff to destination listener without limit check ---
listener_.decNumConnections();
...
new_listener.value().get().onAcceptWorker(std::move(socket_), false, false, network_namespace);
--- onAcceptWorker signature (no limit parameter) ---
Credit
Zheng Yu @ Depthfirst