Skip to content

Commit f641177

Browse files
zijiren233Copilot
andauthored
feat(httpgate): h1/grpc working in same domain (#6464)
* feat(httpgate): same domain grpc router * chore: use map_or replace unwrap_or * fix: grpc need downstream is h2 alpn * fix: check content type with lowercase Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent d52c56f commit f641177

File tree

2 files changed

+60
-143
lines changed

2 files changed

+60
-143
lines changed

service-rs/httpgate/deploy/charts/httpgate/templates/ingress.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{{- if .Values.ingress.enabled -}}
22
---
3-
# HTTP Ingress (devbox- prefix)
3+
# HTTP Ingress (devbox- prefix, non-gRPC requests)
44
apiVersion: networking.k8s.io/v1
55
kind: Ingress
66
metadata:
@@ -55,7 +55,7 @@ spec:
5555
{{- end }}
5656
{{- end }}
5757
---
58-
# gRPC Ingress (devboxgrpc- prefix)
58+
# gRPC Ingress (devbox- prefix, gRPC requests detected by content-type)
5959
apiVersion: networking.k8s.io/v1
6060
kind: Ingress
6161
metadata:
@@ -65,7 +65,8 @@ metadata:
6565
annotations:
6666
nginx.ingress.kubernetes.io/ssl-redirect: "true"
6767
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
68-
higress.io/prefix-match-header-host: devboxgrpc-
68+
higress.io/prefix-match-header-host: devbox-
69+
higress.io/prefix-match-header-content-type: application/grpc
6970
{{- with .Values.ingress.annotations }}
7071
{{- toYaml . | nindent 4 }}
7172
{{- end }}

service-rs/httpgate/src/proxy.rs

Lines changed: 56 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@ use std::sync::Arc;
33
use async_trait::async_trait;
44
use pingora_core::upstreams::peer::{HttpPeer, ALPN};
55
use pingora_core::Result;
6-
use pingora_http::{RequestHeader, ResponseHeader};
6+
use pingora_http::{RequestHeader, ResponseHeader, Version};
77
use pingora_proxy::{ProxyHttp, Session};
88
use regex::Regex;
99
use tracing::{debug, info, warn};
1010

1111
use 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)]
1515
pub 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)]
5757
pub 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
120114
pub 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

Comments
 (0)