1. Introduction
Understanding and testing your server’s maximum concurrent stream configuration is critical for both performance tuning and security hardening against HTTP/2 attacks. This guide provides comprehensive tools and techniques to test the SETTINGS_MAX_CONCURRENT_STREAMS parameter on your web servers.
This article complements our previous guide on Testing Your Website for HTTP/2 Rapid Reset Vulnerabilities from a macOS. While that article focuses on the CVE-2023-44487 Rapid Reset attack, this guide helps you verify that your server properly enforces stream limits, which is a critical defense mechanism.
2. Why Test Stream Limits?
The SETTINGS_MAX_CONCURRENT_STREAMS setting determines how many concurrent requests a client can multiplex over a single HTTP/2 connection. Testing this limit is important because:
- Security validation: Confirms your server enforces reasonable stream limits
- Configuration verification: Ensures your settings match security recommendations (typically 100-128 streams)
- Performance tuning: Helps optimize the balance between throughput and resource consumption
- Attack surface assessment: Identifies if servers accept dangerously high stream counts
3. Understanding HTTP/2 Stream Limits
When an HTTP/2 connection is established, the server sends a SETTINGS frame that includes:
SETTINGS_MAX_CONCURRENT_STREAMS: 100
This tells the client the maximum number of concurrent streams allowed. A compliant client should respect this limit, but attackers will not.
3.1. Common Default Values
Web Servers:
- Nginx: 128 (configurable via
http2_max_concurrent_streams) - Apache: 100 (configurable via
H2MaxSessionStreams) - Caddy: 250 (configurable via
max_concurrent_streams) - LiteSpeed: 100 (configurable in admin panel)
Reverse Proxies and Load Balancers:
- HAProxy: No default limit (should be explicitly configured)
- Envoy: 100 (configurable via
max_concurrent_streams) - Traefik: 250 (configurable via
maxConcurrentStreams)
CDN and Cloud Services:
- CloudFlare: 128 (managed automatically)
- AWS ALB: 128 (managed automatically)
- Azure Front Door: 100 (managed automatically)
4. The Stream Limit Testing Script
The following Python script tests your server’s maximum concurrent streams using the h2 library. This script will:
- Connect to your HTTP/2 server
- Read the advertised
SETTINGS_MAX_CONCURRENT_STREAMSvalue - Attempt to open more streams than the advertised limit
- Verify that the server actually enforces the limit
- Provide detailed results and recommendations
4.1. Prerequisites
Install the required Python libraries:
pip3 install h2 hyper --break-system-packages
Verify installation:
python3 -c "import h2; print(f'h2 version: {h2.__version__}')"
4.2. Complete Script
Save the following as http2_stream_limit_tester.py:
#!/usr/bin/env python3
"""
HTTP/2 Maximum Concurrent Streams Tester
Tests the SETTINGS_MAX_CONCURRENT_STREAMS limit on HTTP/2 servers
and attempts to exceed it to verify enforcement.
Usage:
python3 http2_stream_limit_tester.py --host example.com --port 443
Requirements:
pip3 install h2 hyper --break-system-packages
"""
import argparse
import socket
import ssl
import time
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
try:
from h2.connection import H2Connection
from h2.config import H2Configuration
from h2.events import (
RemoteSettingsChanged,
StreamEnded,
DataReceived,
StreamReset,
WindowUpdated,
SettingsAcknowledged,
ResponseReceived
)
from h2.exceptions import ProtocolError
except ImportError:
print("Error: h2 library not installed")
print("Install with: pip3 install h2 hyper --break-system-packages")
exit(1)
@dataclass
class StreamLimitTestResults:
"""Results from stream limit testing"""
advertised_max_streams: Optional[int] = None
actual_max_streams: int = 0
successful_streams: int = 0
failed_streams: int = 0
reset_streams: int = 0
enforcement_detected: bool = False
test_duration: float = 0.0
server_settings: Dict = field(default_factory=dict)
errors: List[str] = field(default_factory=list)
class HTTP2StreamLimitTester:
"""Test HTTP/2 server stream limits"""
def __init__(
self,
host: str,
port: int = 443,
path: str = "/",
use_tls: bool = True,
timeout: int = 30,
verbose: bool = False
):
self.host = host
self.port = port
self.path = path
self.use_tls = use_tls
self.timeout = timeout
self.verbose = verbose
self.socket: Optional[socket.socket] = None
self.h2_conn: Optional[H2Connection] = None
self.server_max_streams: Optional[int] = None
self.active_streams: Dict[int, dict] = {}
def connect(self) -> bool:
"""Establish connection to the server"""
try:
# Create socket
self.socket = socket.create_connection(
(self.host, self.port),
timeout=self.timeout
)
# Wrap with TLS if needed
if self.use_tls:
context = ssl.create_default_context()
context.check_hostname = True
context.verify_mode = ssl.CERT_REQUIRED
# Set ALPN protocols for HTTP/2
context.set_alpn_protocols(['h2', 'http/1.1'])
self.socket = context.wrap_socket(
self.socket,
server_hostname=self.host
)
# Verify HTTP/2 was negotiated
negotiated_protocol = self.socket.selected_alpn_protocol()
if negotiated_protocol != 'h2':
raise Exception(f"HTTP/2 not negotiated. Got: {negotiated_protocol}")
if self.verbose:
print(f"TLS connection established (ALPN: {negotiated_protocol})")
# Initialize HTTP/2 connection
config = H2Configuration(client_side=True)
self.h2_conn = H2Connection(config=config)
self.h2_conn.initiate_connection()
# Send connection preface
self.socket.sendall(self.h2_conn.data_to_send())
# Receive server settings
self._receive_data()
if self.verbose:
print(f"HTTP/2 connection established to {self.host}:{self.port}")
return True
except Exception as e:
if self.verbose:
print(f"Connection failed: {e}")
return False
def _receive_data(self, timeout: Optional[float] = None) -> List:
"""Receive and process data from server"""
if timeout:
self.socket.settimeout(timeout)
else:
self.socket.settimeout(self.timeout)
events = []
try:
data = self.socket.recv(65536)
if not data:
return events
events_received = self.h2_conn.receive_data(data)
for event in events_received:
events.append(event)
if isinstance(event, RemoteSettingsChanged):
self._handle_settings(event)
elif isinstance(event, ResponseReceived):
if self.verbose:
print(f" Stream {event.stream_id}: Response received")
elif isinstance(event, DataReceived):
if self.verbose:
print(f" Stream {event.stream_id}: Data received ({len(event.data)} bytes)")
elif isinstance(event, StreamEnded):
if self.verbose:
print(f" Stream {event.stream_id}: Ended normally")
if event.stream_id in self.active_streams:
self.active_streams[event.stream_id]['ended'] = True
elif isinstance(event, StreamReset):
if self.verbose:
print(f" Stream {event.stream_id}: Reset (error code: {event.error_code})")
if event.stream_id in self.active_streams:
self.active_streams[event.stream_id]['reset'] = True
# Send any pending data
data_to_send = self.h2_conn.data_to_send()
if data_to_send:
self.socket.sendall(data_to_send)
except socket.timeout:
pass
except Exception as e:
if self.verbose:
print(f"Error receiving data: {e}")
return events
def _handle_settings(self, event: RemoteSettingsChanged):
"""Handle server settings"""
for setting, value in event.changed_settings.items():
setting_name = setting.name if hasattr(setting, 'name') else str(setting)
if self.verbose:
print(f" Server setting: {setting_name} = {value}")
# Check for MAX_CONCURRENT_STREAMS
if 'MAX_CONCURRENT_STREAMS' in setting_name:
self.server_max_streams = value
if self.verbose:
print(f"Server advertises max concurrent streams: {value}")
def send_stream_request(self, stream_id: int) -> bool:
"""Send a GET request on a specific stream"""
try:
headers = [
(':method', 'GET'),
(':path', self.path),
(':scheme', 'https' if self.use_tls else 'http'),
(':authority', self.host),
('user-agent', 'HTTP2-Stream-Limit-Tester/1.0'),
]
self.h2_conn.send_headers(stream_id, headers, end_stream=True)
data_to_send = self.h2_conn.data_to_send()
if data_to_send:
self.socket.sendall(data_to_send)
self.active_streams[stream_id] = {
'sent': time.time(),
'ended': False,
'reset': False
}
return True
except ProtocolError as e:
if self.verbose:
print(f" Stream {stream_id}: Protocol error - {e}")
return False
except Exception as e:
if self.verbose:
print(f" Stream {stream_id}: Failed to send - {e}")
return False
def test_concurrent_streams(
self,
max_streams_to_test: int = 200,
batch_size: int = 10,
delay_between_batches: float = 0.1
) -> StreamLimitTestResults:
"""
Test maximum concurrent streams by opening multiple streams
Args:
max_streams_to_test: Maximum number of streams to attempt
batch_size: Number of streams to open per batch
delay_between_batches: Delay in seconds between batches
"""
results = StreamLimitTestResults()
start_time = time.time()
print(f"\nTesting HTTP/2 Stream Limits:")
print(f" Target: {self.host}:{self.port}")
print(f" Max streams to test: {max_streams_to_test}")
print(f" Batch size: {batch_size}")
print("=" * 60)
try:
# Connect and get initial settings
if not self.connect():
results.errors.append("Failed to establish connection")
return results
results.advertised_max_streams = self.server_max_streams
if self.server_max_streams:
print(f"\nServer advertised limit: {self.server_max_streams} concurrent streams")
else:
print(f"\nServer did not advertise MAX_CONCURRENT_STREAMS limit")
# Start opening streams in batches
stream_id = 1 # HTTP/2 client streams use odd numbers
streams_opened = 0
while streams_opened < max_streams_to_test:
batch_count = min(batch_size, max_streams_to_test - streams_opened)
print(f"\nOpening batch of {batch_count} streams (total: {streams_opened + batch_count})...")
for _ in range(batch_count):
if self.send_stream_request(stream_id):
results.successful_streams += 1
streams_opened += 1
else:
results.failed_streams += 1
stream_id += 2 # Increment by 2 (odd numbers only)
# Process any responses
self._receive_data(timeout=0.5)
# Check for resets
reset_count = sum(1 for s in self.active_streams.values() if s.get('reset', False))
if reset_count > results.reset_streams:
new_resets = reset_count - results.reset_streams
results.reset_streams = reset_count
print(f" WARNING: {new_resets} stream(s) were reset by server")
# If we're getting lots of resets, enforcement is happening
if reset_count > (results.successful_streams * 0.1):
results.enforcement_detected = True
print(f" Stream limit enforcement detected")
# Small delay between batches
if delay_between_batches > 0 and streams_opened < max_streams_to_test:
time.sleep(delay_between_batches)
# Final data reception
print(f"\nWaiting for final responses...")
for _ in range(5):
self._receive_data(timeout=1.0)
# Calculate actual max streams achieved
results.actual_max_streams = results.successful_streams - results.reset_streams
except Exception as e:
results.errors.append(f"Test error: {str(e)}")
if self.verbose:
import traceback
traceback.print_exc()
finally:
results.test_duration = time.time() - start_time
self.close()
return results
def display_results(self, results: StreamLimitTestResults):
"""Display test results"""
print("\n" + "=" * 60)
print("STREAM LIMIT TEST RESULTS")
print("=" * 60)
print(f"\nServer Configuration:")
print(f" Advertised max streams: {results.advertised_max_streams or 'Not specified'}")
print(f"\nTest Statistics:")
print(f" Successful stream opens: {results.successful_streams}")
print(f" Failed stream opens: {results.failed_streams}")
print(f" Streams reset by server: {results.reset_streams}")
print(f" Actual max achieved: {results.actual_max_streams}")
print(f" Test duration: {results.test_duration:.2f}s")
print(f"\nEnforcement:")
if results.enforcement_detected:
print(f" Stream limit enforcement: DETECTED")
else:
print(f" Stream limit enforcement: NOT DETECTED")
print("\n" + "=" * 60)
print("ASSESSMENT")
print("=" * 60)
# Provide recommendations
if results.advertised_max_streams and results.advertised_max_streams > 128:
print(f"\nWARNING: Advertised limit ({results.advertised_max_streams}) exceeds recommended maximum (128)")
print(" Consider reducing http2_max_concurrent_streams")
elif results.advertised_max_streams and results.advertised_max_streams <= 128:
print(f"\nAdvertised limit ({results.advertised_max_streams}) is within recommended range")
if not results.enforcement_detected and results.actual_max_streams > 150:
print(f"\nWARNING: Opened {results.actual_max_streams} streams without enforcement")
print(" Server may be vulnerable to stream exhaustion attacks")
elif results.enforcement_detected:
print(f"\nServer actively enforces stream limits")
print(" Stream limit protection is working correctly")
if results.errors:
print(f"\nErrors encountered:")
for error in results.errors:
print(f" {error}")
print("=" * 60 + "\n")
def close(self):
"""Close the connection"""
try:
if self.h2_conn:
self.h2_conn.close_connection()
if self.socket:
data_to_send = self.h2_conn.data_to_send()
if data_to_send:
self.socket.sendall(data_to_send)
if self.socket:
self.socket.close()
if self.verbose:
print("Connection closed")
except Exception as e:
if self.verbose:
print(f"Error closing connection: {e}")
def main():
parser = argparse.ArgumentParser(
description='Test HTTP/2 server maximum concurrent streams',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Basic test
python3 http2_stream_limit_tester.py --host example.com
# Test with custom parameters
python3 http2_stream_limit_tester.py --host example.com --max-streams 300 --batch 20
# Verbose output
python3 http2_stream_limit_tester.py --host example.com --verbose
# Test specific path
python3 http2_stream_limit_tester.py --host example.com --path /api/health
# Test non-TLS HTTP/2 (h2c)
python3 http2_stream_limit_tester.py --host localhost --port 8080 --no-tls
Prerequisites:
pip3 install h2 hyper --break-system-packages
"""
)
parser.add_argument('--host', required=True, help='Target hostname')
parser.add_argument('--port', type=int, default=443, help='Target port (default: 443)')
parser.add_argument('--path', default='/', help='Request path (default: /)')
parser.add_argument('--no-tls', action='store_true', help='Disable TLS (for h2c testing)')
parser.add_argument('--max-streams', type=int, default=200,
help='Maximum streams to test (default: 200)')
parser.add_argument('--batch', type=int, default=10,
help='Streams per batch (default: 10)')
parser.add_argument('--delay', type=float, default=0.1,
help='Delay between batches in seconds (default: 0.1)')
parser.add_argument('--timeout', type=int, default=30,
help='Connection timeout in seconds (default: 30)')
parser.add_argument('--verbose', action='store_true', help='Enable verbose output')
args = parser.parse_args()
print("=" * 60)
print("HTTP/2 Maximum Concurrent Streams Tester")
print("=" * 60)
tester = HTTP2StreamLimitTester(
host=args.host,
port=args.port,
path=args.path,
use_tls=not args.no_tls,
timeout=args.timeout,
verbose=args.verbose
)
try:
results = tester.test_concurrent_streams(
max_streams_to_test=args.max_streams,
batch_size=args.batch,
delay_between_batches=args.delay
)
tester.display_results(results)
except KeyboardInterrupt:
print("\n\nTest interrupted by user")
except Exception as e:
print(f"\nFatal error: {e}")
if args.verbose:
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()
5. Using the Script
5.1. Basic Usage
Test your server with default settings:
python3 http2_stream_limit_tester.py --host example.com
5.2. Advanced Examples
Test with increased stream count:
python3 http2_stream_limit_tester.py --host example.com --max-streams 300 --batch 20
Verbose output for debugging:
python3 http2_stream_limit_tester.py --host example.com --verbose
Test specific API endpoint:
python3 http2_stream_limit_tester.py --host api.example.com --path /v1/health
Test non-TLS HTTP/2 (h2c):
python3 http2_stream_limit_tester.py --host localhost --port 8080 --no-tls
Gradual escalation test:
# Start conservative
python3 http2_stream_limit_tester.py --host example.com --max-streams 50
# Increase if server handles well
python3 http2_stream_limit_tester.py --host example.com --max-streams 100
# Push to limits
python3 http2_stream_limit_tester.py --host example.com --max-streams 200
Fast burst test:
python3 http2_stream_limit_tester.py --host example.com --max-streams 150 --batch 30 --delay 0.01
Slow ramp test:
python3 http2_stream_limit_tester.py --host example.com --max-streams 200 --batch 5 --delay 0.5
6. Understanding the Results
The script provides detailed output including:
- Advertised max streams: What the server claims to support
- Successful stream opens: How many streams were successfully created
- Failed stream opens: Streams that failed to open
- Streams reset by server: Streams terminated by the server (enforcement)
- Actual max achieved: The real concurrent stream limit
6.1. Example Output
Testing HTTP/2 Stream Limits:
Target: example.com:443
Max streams to test: 200
Batch size: 10
============================================================
Server advertised limit: 128 concurrent streams
Opening batch of 10 streams (total: 10)...
Opening batch of 10 streams (total: 20)...
Opening batch of 10 streams (total: 130)...
WARNING: 5 stream(s) were reset by server
Stream limit enforcement detected
============================================================
STREAM LIMIT TEST RESULTS
============================================================
Server Configuration:
Advertised max streams: 128
Test Statistics:
Successful stream opens: 130
Failed stream opens: 0
Streams reset by server: 5
Actual max achieved: 125
Test duration: 3.45s
Enforcement:
Stream limit enforcement: DETECTED
============================================================
ASSESSMENT
============================================================
Advertised limit (128) is within recommended range
Server actively enforces stream limits
Stream limit protection is working correctly
============================================================
7. Interpreting Different Scenarios
7.1. Scenario 1: Proper Enforcement
Advertised max streams: 100
Successful stream opens: 105
Streams reset by server: 5
Actual max achieved: 100
Stream limit enforcement: DETECTED
Analysis: Server properly enforces the limit. Configuration is working exactly as expected.
7.2. Scenario 2: No Enforcement
Advertised max streams: 128
Successful stream opens: 200
Streams reset by server: 0
Actual max achieved: 200
Stream limit enforcement: NOT DETECTED
Analysis: Server accepts far more streams than advertised. This is a potential vulnerability that should be investigated.
7.3. Scenario 3: No Advertised Limit
Advertised max streams: Not specified
Successful stream opens: 200
Streams reset by server: 0
Actual max achieved: 200
Stream limit enforcement: NOT DETECTED
Analysis: Server does not advertise or enforce limits. High risk configuration that requires immediate remediation.
7.4. Scenario 4: Conservative Limit
Advertised max streams: 50
Successful stream opens: 55
Streams reset by server: 5
Actual max achieved: 50
Stream limit enforcement: DETECTED
Analysis: Very conservative limit. Good for security but may impact performance for legitimate high-throughput applications.
8. Monitoring During Testing
8.1. Server Side Monitoring
While running tests, monitor your server for resource utilization and connection metrics.
Monitor connection states:
netstat -an | grep :443 | awk '{print $6}' | sort | uniq -c
Count active connections:
netstat -an | grep ESTABLISHED | wc -l
Count SYN_RECV connections:
netstat -an | grep SYN_RECV | wc -l
Monitor system resources:
top -l 1 | head -10
8.2. Web Server Specific Monitoring
For Nginx, watch active connections:
watch -n 1 'curl -s http://localhost/nginx_status | grep Active'
For Apache, monitor server status:
watch -n 1 'curl -s http://localhost/server-status | grep requests'
Check HTTP/2 connections:
netstat -an | grep :443 | grep ESTABLISHED | wc -l
Monitor stream counts (if your server exposes this metric):
curl -s http://localhost:9090/metrics | grep http2_streams
Monitor CPU and memory:
top -l 1 | grep -E "CPU|PhysMem"
Check file descriptors:
lsof -i :443 | wc -l
8.3. Using tcpdump
Monitor packets in real time:
# Watch SYN packets
sudo tcpdump -i en0 'tcp[tcpflags] & tcp-syn != 0' -n
# Watch RST packets
sudo tcpdump -i en0 'tcp[tcpflags] & tcp-rst != 0' -n
# Watch specific host and port
sudo tcpdump -i en0 host example.com and port 443 -n
# Save to file for later analysis
sudo tcpdump -i en0 -w test_capture.pcap host example.com
8.4. Using Wireshark
For detailed packet analysis:
# Install Wireshark
brew install --cask wireshark
# Run Wireshark
sudo wireshark
# Or use tshark for command line
tshark -i en0 -f "host example.com"
9. Remediation Steps
If your tests reveal issues, apply these configuration fixes:
9.1. Nginx Configuration
http {
# Set conservative concurrent stream limit
http2_max_concurrent_streams 100;
# Additional protections
http2_recv_timeout 10s;
http2_idle_timeout 30s;
http2_max_field_size 16k;
http2_max_header_size 32k;
}
9.2. Apache Configuration
Set in httpd.conf or virtual host configuration:
# Set maximum concurrent streams
H2MaxSessionStreams 100
# Additional HTTP/2 settings
H2StreamTimeout 10
H2MinWorkers 10
H2MaxWorkers 150
H2StreamMaxMemSize 65536
9.3. HAProxy Configuration
defaults
timeout http-request 10s
timeout http-keep-alive 10s
frontend fe_main
bind :443 ssl crt /path/to/cert.pem alpn h2,http/1.1
# Limit streams per connection
http-request track-sc0 src table connection_limit
http-request deny if { sc_conn_cur(0) gt 100 }
9.4. Envoy Configuration
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 443
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http2_protocol_options:
max_concurrent_streams: 100
initial_stream_window_size: 65536
initial_connection_window_size: 1048576
9.5. Caddy Configuration
example.com {
encode gzip
# HTTP/2 settings
protocol {
experimental_http3
max_concurrent_streams 100
}
reverse_proxy localhost:8080
}
10. Combining with Rapid Reset Testing
You can use both the stream limit tester and the Rapid Reset tester together for comprehensive HTTP/2 security assessment:
# Step 1: Test stream limits
python3 http2_stream_limit_tester.py --host example.com
# Step 2: Test rapid reset with IP spoofing
sudo python3 http2rapidresettester_macos.py \
--host example.com \
--cidr 192.168.1.0/24 \
--packets 1000
# Step 3: Re-test stream limits to verify no degradation
python3 http2_stream_limit_tester.py --host example.com
11. Security Best Practices
11.1. Configuration Guidelines
- Set explicit limits: Never rely on default values
- Use conservative values: 100-128 streams is the recommended range
- Monitor enforcement: Regularly verify that limits are actually being enforced
- Document settings: Maintain records of your stream limit configuration
- Test after changes: Always test after configuration modifications
11.2. Defense in Depth
Stream limits should be one layer in a comprehensive security strategy:
- Stream limits: Prevent excessive concurrent streams per connection
- Connection limits: Limit total connections per IP address
- Request rate limiting: Throttle requests per second
- Resource quotas: Set memory and CPU limits
- WAF/DDoS protection: Use cloud-based or on-premise DDoS mitigation
11.3. Regular Testing Schedule
Establish a regular testing schedule:
- Weekly: Automated basic stream limit tests
- Monthly: Comprehensive security testing including Rapid Reset
- After changes: Always test after configuration or infrastructure changes
- Quarterly: Full security audit including penetration testing
12. Troubleshooting
12.1. Common Errors
Error: “SSL: CERTIFICATE_VERIFY_FAILED”
This occurs when testing against servers with self-signed certificates. For testing purposes only, you can modify the script to skip certificate verification (not recommended for production testing).
Error: “h2 library not installed”
Install the required library:
pip3 install h2 hyper --break-system-packages
Error: “Connection refused”
Verify the port is open:
telnet example.com 443
Check if HTTP/2 is enabled:
curl -I --http2 https://example.com
Error: “HTTP/2 not negotiated”
The server may not support HTTP/2. Verify with:
curl -I --http2 https://example.com | grep -i http/2
12.2. No Streams Being Reset
If streams are not being reset despite exceeding the advertised limit:
- Server may not be enforcing limits properly
- Configuration may not have been applied (restart required)
- Server may be using a different enforcement mechanism
- Limits may be set at a different layer (load balancer vs web server)
12.3. High Failure Rate
If many streams fail to open:
- Network connectivity issues
- Firewall blocking requests
- Server resource exhaustion
- Rate limiting triggering prematurely
13. Understanding the Attack Surface
When testing your infrastructure, consider all HTTP/2 endpoints:
- Web servers: Nginx, Apache, IIS
- Load balancers: HAProxy, Envoy, ALB
- API gateways: Kong, Tyk, AWS API Gateway
- CDN endpoints: CloudFlare, Fastly, Akamai
- Reverse proxies: Traefik, Caddy
13.1. Testing Strategy
Test at multiple layers:
# Test CDN edge
python3 http2_stream_limit_tester.py --host cdn.example.com
# Test load balancer directly
python3 http2_stream_limit_tester.py --host lb.example.com
# Test origin server
python3 http2_stream_limit_tester.py --host origin.example.com
14. Conclusion
Testing your HTTP/2 maximum concurrent streams configuration is essential for maintaining a secure and performant web infrastructure. This tool allows you to:
- Verify that your server advertises appropriate stream limits
- Confirm that advertised limits are actually enforced
- Identify misconfigurations before they can be exploited
- Tune performance while maintaining security
Regular testing, combined with proper configuration and monitoring, will help protect your infrastructure against HTTP/2-based attacks while maintaining optimal performance for legitimate users.
15. Additional Resources
- HTTP/2 RFC 7540
- SETTINGS Frame Documentation
- CVE-2023-44487 (Rapid Reset)
- Testing Your Website for HTTP/2 Rapid Reset Vulnerabilities
- Nginx HTTP/2 Module Documentation
- Apache mod_http2 Documentation
This guide and testing script are provided for educational and defensive security purposes only. Always obtain proper authorization before testing systems you do not own.