fix(rustproxy-routing): allow QUIC UDP TLS connections without SNI to match domain-restricted routes
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-20 - 25.17.1 - fix(rustproxy-routing)
|
||||||
|
allow QUIC UDP TLS connections without SNI to match domain-restricted routes
|
||||||
|
|
||||||
|
- Exempts UDP transport from the no-SNI rejection logic because QUIC encrypts the TLS ClientHello and SNI is unavailable at accept time
|
||||||
|
- Adds regression tests to confirm QUIC route matching succeeds without SNI while TCP TLS without SNI remains rejected
|
||||||
|
|
||||||
## 2026-03-19 - 25.17.0 - feat(rustproxy-passthrough)
|
## 2026-03-19 - 25.17.0 - feat(rustproxy-passthrough)
|
||||||
add PROXY protocol v2 client IP handling for UDP and QUIC listeners
|
add PROXY protocol v2 client IP handling for UDP and QUIC listeners
|
||||||
|
|
||||||
|
|||||||
@@ -122,10 +122,16 @@ impl RouteManager {
|
|||||||
// This prevents session-ticket resumption from misrouting when clients
|
// This prevents session-ticket resumption from misrouting when clients
|
||||||
// omit SNI (RFC 8446 recommends but doesn't mandate SNI on resumption).
|
// omit SNI (RFC 8446 recommends but doesn't mandate SNI on resumption).
|
||||||
// Wildcard-only routes (domains: ["*"]) still match since they accept all.
|
// Wildcard-only routes (domains: ["*"]) still match since they accept all.
|
||||||
let patterns = domains.to_vec();
|
//
|
||||||
let is_wildcard_only = patterns.iter().all(|d| *d == "*");
|
// Exception: QUIC (UDP transport) encrypts the TLS ClientHello, so SNI
|
||||||
if !is_wildcard_only {
|
// is unavailable at accept time. Domain verification happens per-request
|
||||||
return false;
|
// in H3ProxyService via the :authority header.
|
||||||
|
if ctx.transport != Some(TransportProtocol::Udp) {
|
||||||
|
let patterns = domains.to_vec();
|
||||||
|
let is_wildcard_only = patterns.iter().all(|d| *d == "*");
|
||||||
|
if !is_wildcard_only {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -997,4 +1003,52 @@ mod tests {
|
|||||||
let result = manager.find_route(&udp_ctx).unwrap();
|
let result = manager.find_route(&udp_ctx).unwrap();
|
||||||
assert_eq!(result.route.name.as_deref(), Some("udp-route"));
|
assert_eq!(result.route.name.as_deref(), Some("udp-route"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quic_tls_no_sni_matches_domain_restricted_route() {
|
||||||
|
// QUIC accept-level matching: is_tls=true, domain=None, transport=Udp.
|
||||||
|
// Should match because QUIC encrypts the ClientHello — SNI is unavailable
|
||||||
|
// at accept time but verified per-request in H3ProxyService.
|
||||||
|
let mut route = make_route(443, Some("example.com"), 0);
|
||||||
|
route.route_match.transport = Some(TransportProtocol::Udp);
|
||||||
|
let routes = vec![route];
|
||||||
|
let manager = RouteManager::new(routes);
|
||||||
|
|
||||||
|
let ctx = MatchContext {
|
||||||
|
port: 443,
|
||||||
|
domain: None,
|
||||||
|
path: None,
|
||||||
|
client_ip: None,
|
||||||
|
tls_version: None,
|
||||||
|
headers: None,
|
||||||
|
is_tls: true,
|
||||||
|
protocol: Some("quic"),
|
||||||
|
transport: Some(TransportProtocol::Udp),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(manager.find_route(&ctx).is_some(),
|
||||||
|
"QUIC (UDP) with is_tls=true and domain=None should match domain-restricted routes");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tcp_tls_no_sni_still_rejects_domain_restricted_route() {
|
||||||
|
// TCP TLS without SNI must still be rejected (no QUIC exemption).
|
||||||
|
let routes = vec![make_route(443, Some("example.com"), 0)];
|
||||||
|
let manager = RouteManager::new(routes);
|
||||||
|
|
||||||
|
let ctx = MatchContext {
|
||||||
|
port: 443,
|
||||||
|
domain: None,
|
||||||
|
path: None,
|
||||||
|
client_ip: None,
|
||||||
|
tls_version: None,
|
||||||
|
headers: None,
|
||||||
|
is_tls: true,
|
||||||
|
protocol: None,
|
||||||
|
transport: None, // TCP (default)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(manager.find_route(&ctx).is_none(),
|
||||||
|
"TCP TLS without SNI should NOT match domain-restricted routes");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '25.17.0',
|
version: '25.17.1',
|
||||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user