Skip to content

Commit b58e24c

Browse files
authored
fix(supervisor): report plaintext scheme to middleware (#4397)
Closes #4253 Signed-off-by: Eric Curtin <eric.curtin@docker.com>
1 parent fdfbf84 commit b58e24c

2 files changed

Lines changed: 124 additions & 4 deletions

File tree

‎crates/openshell-supervisor-network/src/l7/middleware.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ pub async fn apply_middleware_chain_with_request_id<C: AsyncRead + AsyncWrite +
521521
req,
522522
client,
523523
ctx,
524-
"https",
524+
ctx.request_scheme(),
525525
chain,
526526
runner,
527527
generation_guard,

‎crates/openshell-supervisor-network/src/l7/relay.rs‎

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@ pub struct L7EvalContext {
8787
pub(crate) endpoint_observation_tx: Option<EndpointObservationSender>,
8888
}
8989

90+
impl L7EvalContext {
91+
/// Whether the inspected transport is plaintext rather than TLS-terminated.
92+
fn is_plaintext(&self) -> bool {
93+
self.request_default_port == Some(80)
94+
}
95+
96+
/// HTTP scheme of the inspected transport.
97+
pub(crate) fn request_scheme(&self) -> &'static str {
98+
if self.is_plaintext() { "http" } else { "https" }
99+
}
100+
101+
/// WebSocket scheme of the inspected transport.
102+
pub(crate) fn websocket_scheme(&self) -> &'static str {
103+
if self.is_plaintext() { "ws" } else { "wss" }
104+
}
105+
}
106+
90107
fn request_default_port(ctx: &L7EvalContext) -> Option<u16> {
91108
ctx.request_default_port
92109
}
@@ -731,7 +748,7 @@ where
731748
Some(http_response_middleware_relay(
732749
&request,
733750
ctx,
734-
"https",
751+
ctx.request_scheme(),
735752
request_id,
736753
response_chain,
737754
engine.middleware_runner(),
@@ -1469,7 +1486,7 @@ where
14691486
chain,
14701487
engine.middleware_runner(),
14711488
ctx,
1472-
"wss",
1489+
ctx.websocket_scheme(),
14731490
)
14741491
.await;
14751492
let preflight = match preflight {
@@ -2216,7 +2233,7 @@ where
22162233
chain,
22172234
engine.middleware_runner(),
22182235
ctx,
2219-
"wss",
2236+
ctx.websocket_scheme(),
22202237
)
22212238
.await;
22222239
let preflight = match preflight {
@@ -7392,6 +7409,109 @@ network_policies:
73927409
}
73937410
}
73947411

7412+
#[test]
7413+
fn inspected_schemes_follow_transport() {
7414+
for (port, http, ws) in [
7415+
(Some(80), "http", "ws"),
7416+
(Some(443), "https", "wss"),
7417+
(None, "https", "wss"),
7418+
] {
7419+
let ctx = L7EvalContext {
7420+
request_default_port: port,
7421+
..Default::default()
7422+
};
7423+
assert_eq!(ctx.request_scheme(), http);
7424+
assert_eq!(ctx.websocket_scheme(), ws);
7425+
}
7426+
}
7427+
7428+
/// Records the scheme each request reaches middleware with.
7429+
struct SchemeRecorder(Arc<std::sync::Mutex<Vec<String>>>);
7430+
7431+
#[tonic::async_trait]
7432+
impl openshell_core::middleware::InProcessMiddleware for SchemeRecorder {
7433+
async fn describe(&self) -> openshell_core::proto::MiddlewareManifest {
7434+
LimitService {
7435+
name: "test/scheme",
7436+
max_body_bytes: 1024,
7437+
replacement: None,
7438+
}
7439+
.describe()
7440+
.await
7441+
}
7442+
7443+
async fn validate_config(
7444+
&self,
7445+
_middleware_name: &str,
7446+
_config: &prost_types::Struct,
7447+
) -> Result<()> {
7448+
Ok(())
7449+
}
7450+
7451+
async fn evaluate_http_request(
7452+
&self,
7453+
request: openshell_core::middleware::HttpRequestView<'_>,
7454+
) -> Result<openshell_core::proto::HttpRequestResult> {
7455+
self.0.lock().unwrap().push(request.target().scheme.clone());
7456+
Ok(openshell_core::proto::HttpRequestResult {
7457+
decision: openshell_core::proto::Decision::Allow as i32,
7458+
..Default::default()
7459+
})
7460+
}
7461+
}
7462+
7463+
#[tokio::test]
7464+
async fn middleware_request_scheme_matches_inspected_transport() {
7465+
use openshell_supervisor_middleware::{ChainEntry, ChainRunner, OnError};
7466+
7467+
let (_config, tunnel_engine, ctx) =
7468+
middleware_relay_context("openshell/regex", "fail_closed");
7469+
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
7470+
let registry = openshell_supervisor_middleware::MiddlewareRegistry::connect_services(
7471+
vec![Arc::new(SchemeRecorder(seen.clone()))],
7472+
Vec::new(),
7473+
)
7474+
.await
7475+
.expect("connect named middleware services");
7476+
let runner = ChainRunner::from_registry(registry);
7477+
let chain = vec![ChainEntry {
7478+
name: "scheme".into(),
7479+
implementation: "test/scheme".into(),
7480+
order: 0,
7481+
config: prost_types::Struct::default(),
7482+
on_error: OnError::FailClosed,
7483+
}];
7484+
7485+
for (port, scheme) in [(Some(80), "http"), (Some(443), "https")] {
7486+
let ctx = L7EvalContext {
7487+
request_default_port: port,
7488+
..ctx.clone()
7489+
};
7490+
let req = crate::l7::provider::L7Request {
7491+
action: "GET".into(),
7492+
target: "/v1/messages".into(),
7493+
query_params: std::collections::HashMap::new(),
7494+
raw_header: b"GET /v1/messages HTTP/1.1\r\nHost: api.example.test\r\n\r\n".to_vec(),
7495+
body_length: crate::l7::provider::BodyLength::None,
7496+
};
7497+
let (_app, mut client) = tokio::io::duplex(8192);
7498+
let result = apply_middleware_chain_with_request_id(
7499+
req,
7500+
&mut client,
7501+
&ctx,
7502+
chain.clone(),
7503+
&runner,
7504+
tunnel_engine.generation_guard(),
7505+
openshell_supervisor_middleware::TransformedBodyPolicy::NotPolicyRelevant,
7506+
"test-request-id",
7507+
)
7508+
.await
7509+
.expect("apply middleware chain");
7510+
assert!(matches!(result, MiddlewareApplyResult::Allowed(_)));
7511+
assert_eq!(seen.lock().unwrap().pop().as_deref(), Some(scheme));
7512+
}
7513+
}
7514+
73957515
#[tokio::test]
73967516
async fn all_unresolved_fail_open_forwards_body_unbuffered() {
73977517
// A chain whose only entry is an unregistered binding has no resolvable

0 commit comments

Comments
 (0)