@@ -3,19 +3,19 @@ use std::sync::Arc;
33use async_trait:: async_trait;
44use pingora_core:: upstreams:: peer:: { HttpPeer , ALPN } ;
55use pingora_core:: Result ;
6- use pingora_http:: { RequestHeader , ResponseHeader } ;
6+ use pingora_http:: { RequestHeader , ResponseHeader , Version } ;
77use pingora_proxy:: { ProxyHttp , Session } ;
88use regex:: Regex ;
99use tracing:: { debug, info, warn} ;
1010
1111use crate :: registry:: DevboxRegistry ;
1212
13- /// Upstream protocol type based on host prefix
13+ /// Upstream protocol type based on incoming request
1414#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
1515pub enum UpstreamProtocol {
16- /// HTTP/1.1 over cleartext (prefix: devbox-)
16+ /// HTTP/1.1 over cleartext
1717 Http ,
18- /// gRPC over HTTP/2 cleartext (prefix: devboxgrpc- )
18+ /// gRPC over HTTP/2 cleartext (detected by H2 + content-type: application/grpc* )
1919 Grpc ,
2020}
2121
@@ -39,9 +39,9 @@ const BODY_NOT_RUNNING: &[u8] = b"devbox not running";
3939/// - uniqueID: lowercase alphanumeric with hyphens, cannot start/end with hyphen
4040/// - port: numeric or "agent" (special keyword for agent port)
4141///
42- /// Note: Prefix (e.g., "devbox-", "devboxgrpc-") should be stripped before matching.
42+ /// Note: "devbox-" prefix should be stripped before matching.
4343///
44- /// Examples (after prefix stripped):
44+ /// Examples (after devbox- prefix stripped):
4545/// - "outdoor-before-78648-8080.devbox.xxx" -> ("outdoor-before-78648", 8080)
4646/// - "my-app-8080.devbox.xxx" -> ("my-app", 8080)
4747/// - "my-app-agent.devbox.xxx" -> ("my-app", agent_port from config)
@@ -52,7 +52,7 @@ static HOST_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
5252 Regex :: new ( r"^([a-z\d](?:[-a-z\d]*[a-z\d])?)-(\d+|agent)\." ) . unwrap ( )
5353} ) ;
5454
55- /// Host parser for extracting protocol, uniqueID and port from Host header.
55+ /// Host parser for extracting uniqueID and port from Host header.
5656#[ derive( Debug , Clone ) ]
5757pub struct HostParser {
5858 /// Agent port (used when port is "agent" instead of a number)
@@ -65,29 +65,20 @@ impl HostParser {
6565 Self { agent_port }
6666 }
6767
68- /// Parse the Host header to extract protocol, uniqueID and port.
68+ /// Parse the Host header to extract uniqueID and port.
6969 ///
70- /// Expected formats:
71- /// - `devbox-<uniqueID>-<port>.xxx[:port]` -> HTTP
72- /// - `devboxgrpc-<uniqueID>-<port>.xxx[:port]` -> gRPCs
70+ /// Expected format: `devbox-<uniqueID>-<port>.xxx[:port]`
7371 ///
7472 /// Examples:
75- /// - `devbox-outdoor-before-78648-8080.devbox.sealos.io` -> (Http, "outdoor-before-78648", 8080)
76- /// - `devboxgrpc -my-app-50051.devbox.sealos.io` -> (Grpc, "my-app", 50051)
77- /// - `devbox-my-app-agent.devbox.sealos.io` -> (Http, "my-app", agent_port)
78- pub fn parse ( & self , host : & str ) -> Option < ( UpstreamProtocol , String , u16 ) > {
73+ /// - `devbox-outdoor-before-78648-8080.devbox.sealos.io` -> ("outdoor-before-78648", 8080)
74+ /// - `devbox -my-app-50051.devbox.sealos.io` -> ("my-app", 50051)
75+ /// - `devbox-my-app-agent.devbox.sealos.io` -> ("my-app", agent_port)
76+ pub fn parse ( & self , host : & str ) -> Option < ( String , u16 ) > {
7977 // Remove port suffix if present (e.g., "xxx:443" -> "xxx")
8078 let host_without_port = host. split ( ':' ) . next ( ) . unwrap_or ( host) ;
8179
82- // Try to strip prefixes and determine protocol
83- let ( protocol, host_stripped) =
84- if let Some ( stripped) = host_without_port. strip_prefix ( "devboxgrpc-" ) {
85- ( UpstreamProtocol :: Grpc , stripped)
86- } else if let Some ( stripped) = host_without_port. strip_prefix ( "devbox-" ) {
87- ( UpstreamProtocol :: Http , stripped)
88- } else {
89- ( UpstreamProtocol :: Http , host_without_port)
90- } ;
80+ // Strip devbox- prefix
81+ let host_stripped = host_without_port. strip_prefix ( "devbox-" ) ?;
9182
9283 HOST_REGEX . captures ( host_stripped) . and_then ( |caps| {
9384 let unique_id = caps. get ( 1 ) ?. as_str ( ) . to_string ( ) ;
@@ -97,7 +88,7 @@ impl HostParser {
9788 } else {
9889 port_str. parse ( ) . ok ( ) ?
9990 } ;
100- Some ( ( protocol , unique_id, port) )
91+ Some ( ( unique_id, port) )
10192 } )
10293 }
10394}
@@ -115,11 +106,14 @@ pub struct ProxyCtx {
115106/// Pingora-based HTTP proxy for routing requests to devbox pods.
116107///
117108/// Routes requests based on the Host header pattern:
118- /// - `devbox-<uniqueID>-<port>.xxx` -> HTTP/1.1 to `<pod_ip>:<port>`
119- /// - `devboxgrpc-<uniqueID>-<port>.xxx` -> gRPCs to `<pod_ip>:<port>`
109+ /// - `devbox-<uniqueID>-<port>.xxx` -> `<pod_ip>:<port>`
110+ ///
111+ /// Protocol detection:
112+ /// - gRPC/H2: detected by HTTP/2 request with content-type starting with "application/grpc"
113+ /// - HTTP/1.1: all other requests
120114pub struct DevboxProxy {
121115 registry : Arc < DevboxRegistry > ,
122- /// Host parser for extracting protocol, uniqueID and port from Host header
116+ /// Host parser for extracting uniqueID and port from Host header
123117 host_parser : HostParser ,
124118}
125119
@@ -211,12 +205,28 @@ impl ProxyHttp for DevboxProxy {
211205 . or_else ( || session. req_header ( ) . uri . authority ( ) . map ( |a| a. as_str ( ) ) )
212206 . unwrap_or ( "" ) ;
213207
214- // Parse protocol, uniqueID and port from host
215- let Some ( ( protocol , unique_id, port) ) = self . host_parser . parse ( host) else {
208+ // Parse uniqueID and port from host
209+ let Some ( ( unique_id, port) ) = self . host_parser . parse ( host) else {
216210 warn ! ( host = %host, "Failed to parse host header" ) ;
217211 return Self :: send_not_found ( session) . await ;
218212 } ;
219213
214+ // Detect protocol: use gRPC/H2 when request is HTTP/2 AND content-type starts with "application/grpc"
215+ let is_h2 = session. req_header ( ) . version == Version :: HTTP_2 ;
216+ let is_grpc_content_type = session
217+ . req_header ( )
218+ . headers
219+ . get ( "content-type" )
220+ . and_then ( |ct| ct. to_str ( ) . ok ( ) )
221+ . map ( |ct| ct. to_ascii_lowercase ( ) )
222+ . is_some_and ( |ct| ct. starts_with ( "application/grpc" ) ) ;
223+
224+ let protocol = if is_h2 && is_grpc_content_type {
225+ UpstreamProtocol :: Grpc
226+ } else {
227+ UpstreamProtocol :: Http
228+ } ;
229+
220230 // Resolve backend from registry
221231 let ( backend_ip, backend_port) = match self . resolve_backend ( & unique_id, port) {
222232 BackendResult :: Ok ( ip, port) => ( ip, port) ,
@@ -303,145 +313,57 @@ mod tests {
303313 HostParser :: new ( TEST_AGENT_PORT )
304314 }
305315
306- // HTTP protocol tests (devbox- prefix)
316+ // Host parsing tests (devbox- prefix)
307317
308318 #[ test]
309- fn test_parse_host_http_standard_format ( ) {
319+ fn test_parse_host_standard_format ( ) {
310320 let parser = test_parser ( ) ;
311321 let result = parser. parse ( "devbox-outdoor-before-78648-8080.devbox.sealos.io" ) ;
312- assert_eq ! (
313- result,
314- Some ( (
315- UpstreamProtocol :: Http ,
316- "outdoor-before-78648" . to_string( ) ,
317- 8080
318- ) )
319- ) ;
322+ assert_eq ! ( result, Some ( ( "outdoor-before-78648" . to_string( ) , 8080 ) ) ) ;
320323 }
321324
322325 #[ test]
323- fn test_parse_host_http_simple_id ( ) {
326+ fn test_parse_host_simple_id ( ) {
324327 let parser = test_parser ( ) ;
325328 let result = parser. parse ( "devbox-my-app-8080.devbox.sealos.io" ) ;
326- assert_eq ! (
327- result,
328- Some ( ( UpstreamProtocol :: Http , "my-app" . to_string( ) , 8080 ) )
329- ) ;
329+ assert_eq ! ( result, Some ( ( "my-app" . to_string( ) , 8080 ) ) ) ;
330330 }
331331
332332 #[ test]
333- fn test_parse_host_http_single_word ( ) {
333+ fn test_parse_host_single_word ( ) {
334334 let parser = test_parser ( ) ;
335335 let result = parser. parse ( "devbox-myapp-443.devbox.sealos.io" ) ;
336- assert_eq ! (
337- result,
338- Some ( ( UpstreamProtocol :: Http , "myapp" . to_string( ) , 443 ) )
339- ) ;
336+ assert_eq ! ( result, Some ( ( "myapp" . to_string( ) , 443 ) ) ) ;
340337 }
341338
342339 #[ test]
343- fn test_parse_host_http_with_numbers ( ) {
340+ fn test_parse_host_with_numbers ( ) {
344341 let parser = test_parser ( ) ;
345342 let result = parser. parse ( "devbox-app123-test456-3000.devbox.sealos.io" ) ;
346- assert_eq ! (
347- result,
348- Some ( ( UpstreamProtocol :: Http , "app123-test456" . to_string( ) , 3000 ) )
349- ) ;
343+ assert_eq ! ( result, Some ( ( "app123-test456" . to_string( ) , 3000 ) ) ) ;
350344 }
351345
352346 #[ test]
353- fn test_parse_host_http_with_port_suffix ( ) {
347+ fn test_parse_host_with_port_suffix ( ) {
354348 let parser = test_parser ( ) ;
355349 let result = parser. parse ( "devbox-outdoor-before-78648-8080.devbox.sealos.io:443" ) ;
356- assert_eq ! (
357- result,
358- Some ( (
359- UpstreamProtocol :: Http ,
360- "outdoor-before-78648" . to_string( ) ,
361- 8080
362- ) )
363- ) ;
350+ assert_eq ! ( result, Some ( ( "outdoor-before-78648" . to_string( ) , 8080 ) ) ) ;
364351 }
365352
366353 #[ test]
367- fn test_parse_host_http_agent_port ( ) {
354+ fn test_parse_host_agent_port ( ) {
368355 let parser = test_parser ( ) ;
369356 let result = parser. parse ( "devbox-my-app-agent.devbox.sealos.io" ) ;
370- assert_eq ! (
371- result,
372- Some ( (
373- UpstreamProtocol :: Http ,
374- "my-app" . to_string( ) ,
375- TEST_AGENT_PORT
376- ) )
377- ) ;
357+ assert_eq ! ( result, Some ( ( "my-app" . to_string( ) , TEST_AGENT_PORT ) ) ) ;
378358 }
379359
380360 #[ test]
381- fn test_parse_host_http_agent_port_complex_id ( ) {
361+ fn test_parse_host_agent_port_complex_id ( ) {
382362 let parser = test_parser ( ) ;
383363 let result = parser. parse ( "devbox-outdoor-before-78648-agent.devbox.sealos.io" ) ;
384364 assert_eq ! (
385365 result,
386- Some ( (
387- UpstreamProtocol :: Http ,
388- "outdoor-before-78648" . to_string( ) ,
389- TEST_AGENT_PORT
390- ) )
391- ) ;
392- }
393-
394- // gRPC protocol tests (devboxgrpc- prefix)
395-
396- #[ test]
397- fn test_parse_host_grpc_standard_format ( ) {
398- let parser = test_parser ( ) ;
399- let result = parser. parse ( "devboxgrpc-outdoor-before-78648-50051.devbox.sealos.io" ) ;
400- assert_eq ! (
401- result,
402- Some ( (
403- UpstreamProtocol :: Grpc ,
404- "outdoor-before-78648" . to_string( ) ,
405- 50051
406- ) )
407- ) ;
408- }
409-
410- #[ test]
411- fn test_parse_host_grpc_simple_id ( ) {
412- let parser = test_parser ( ) ;
413- let result = parser. parse ( "devboxgrpc-my-app-50051.devbox.sealos.io" ) ;
414- assert_eq ! (
415- result,
416- Some ( ( UpstreamProtocol :: Grpc , "my-app" . to_string( ) , 50051 ) )
417- ) ;
418- }
419-
420- #[ test]
421- fn test_parse_host_grpc_with_port_suffix ( ) {
422- let parser = test_parser ( ) ;
423- let result = parser. parse ( "devboxgrpc-outdoor-before-78648-50051.devbox.sealos.io:443" ) ;
424- assert_eq ! (
425- result,
426- Some ( (
427- UpstreamProtocol :: Grpc ,
428- "outdoor-before-78648" . to_string( ) ,
429- 50051
430- ) )
431- ) ;
432- }
433-
434- #[ test]
435- fn test_parse_host_grpc_agent_port ( ) {
436- let parser = test_parser ( ) ;
437- let result = parser. parse ( "devboxgrpc-my-app-agent.devbox.sealos.io" ) ;
438- assert_eq ! (
439- result,
440- Some ( (
441- UpstreamProtocol :: Grpc ,
442- "my-app" . to_string( ) ,
443- TEST_AGENT_PORT
444- ) )
366+ Some ( ( "outdoor-before-78648" . to_string( ) , TEST_AGENT_PORT ) )
445367 ) ;
446368 }
447369
@@ -453,9 +375,6 @@ mod tests {
453375 assert ! ( parser
454376 . parse( "devbox-outdoor-before.devbox.sealos.io" )
455377 . is_none( ) ) ;
456- assert ! ( parser
457- . parse( "devboxgrpc-outdoor-before.devbox.sealos.io" )
458- . is_none( ) ) ;
459378 }
460379
461380 #[ test]
@@ -464,16 +383,13 @@ mod tests {
464383 // No prefix
465384 assert ! ( parser. parse( "invalid.example.com" ) . is_none( ) ) ;
466385 assert ! ( parser. parse( "" ) . is_none( ) ) ;
467- // Missing prefix
386+ // Missing devbox- prefix
468387 assert ! ( parser
469388 . parse( "outdoor-before-78648-8080.devbox.sealos.io" )
470389 . is_none( ) ) ;
471390 // Invalid uniqueID format (starts/ends with hyphen)
472391 assert ! ( parser. parse( "devbox--invalid-8080.devbox.io" ) . is_none( ) ) ;
473392 assert ! ( parser. parse( "devbox-invalid--8080.devbox.io" ) . is_none( ) ) ;
474- assert ! ( parser
475- . parse( "devboxgrpc--invalid-50051.devbox.io" )
476- . is_none( ) ) ;
477393 }
478394
479395 #[ test]
0 commit comments