blob: e5247d1c302ecd69f6fe1418ca06e0d44553aa7b [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use futures_util::{SinkExt, StreamExt};
use maka_agent_facts::configuration::policy::NetworkProxy;
use maka_agent_facts::configuration::policy::ProxyProtocol;
use maka_network::{Policy, connect_websocket};
use reqwest::{
Certificate,
header::{AUTHORIZATION, HeaderMap, HeaderValue},
};
use std::{sync::Arc, time::Duration};
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
net::TcpListener,
};
use tokio_rustls::{
TlsAcceptor,
rustls::{
ServerConfig,
crypto::aws_lc_rs,
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
},
};
use tokio_tungstenite::{
accept_hdr_async,
tungstenite::{
Message,
handshake::server::{Request, Response},
},
};
const CA: &[u8] = include_bytes!("fixtures/ca.pem");
const CERT: &[u8] = include_bytes!("fixtures/localhost.pem");
const KEY: &[u8] = include_bytes!("fixtures/localhost.key");
trait Io: AsyncRead + AsyncWrite + Unpin + Send {}
impl<T: AsyncRead + AsyncWrite + Unpin + Send> Io for T {}
fn acceptor() -> TlsAcceptor {
let config = ServerConfig::builder_with_provider(Arc::new(aws_lc_rs::default_provider()))
.with_safe_default_protocol_versions()
.unwrap()
.with_no_client_auth()
.with_single_cert(
vec![CertificateDer::from_pem_slice(CERT).unwrap()],
PrivateKeyDer::from_pem_slice(KEY).unwrap(),
)
.unwrap();
TlsAcceptor::from(Arc::new(config))
}
async fn headers(stream: &mut (impl AsyncRead + Unpin)) -> String {
let mut bytes = Vec::new();
while !bytes.ends_with(b"\r\n\r\n") {
assert!(bytes.len() < 16 * 1024);
bytes.push(stream.read_u8().await.unwrap());
}
String::from_utf8(bytes).unwrap().to_ascii_lowercase()
}
#[derive(Clone, Copy, PartialEq)]
enum Trust {
Valid,
Untrusted,
WrongName,
}
#[expect(
clippy::result_large_err,
reason = "tungstenite fixes its handshake callback error type"
)]
async fn exchange(protocol: ProxyProtocol, websocket: bool, trust: Trust) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let policy = Policy::from_settings(
&NetworkProxy {
enabled: true,
protocol,
host: "localhost".into(),
port: listener.local_addr().unwrap().port(),
auth_enabled: true,
username: "user".into(),
bypass_list: vec![],
auto_bypass_domains: vec![],
},
Some("proxy-password"),
)
.unwrap();
let destination = if trust == Trust::WrongName {
"wrong.maka.invalid"
} else {
"models.maka.invalid"
};
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let tls = acceptor();
let mut proxy: Box<dyn Io> = if protocol == ProxyProtocol::Https {
match tls.accept(tcp).await {
Ok(stream) => Box::new(stream),
Err(_) => {
assert!(trust == Trust::Untrusted);
return;
}
}
} else {
Box::new(tcp)
};
let connect = headers(&mut proxy).await;
assert!(connect.starts_with(&format!("connect {destination}:443 http/1.1\r\n")));
assert!(
connect.contains("\r\nproxy-authorization: basic dxnlcjpwcm94es1wyxnzd29yza==\r\n")
);
assert!(
!connect.contains("\r\nauthorization:"),
"model key must not reach CONNECT headers"
);
proxy
.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
.await
.unwrap();
let stream = tls.accept(proxy).await;
if trust != Trust::Valid {
assert!(stream.is_err(), "invalid origin certificate accepted");
return;
}
let mut stream = stream.unwrap();
if websocket {
let mut socket = accept_hdr_async(stream, |request: &Request, response: Response| {
assert_eq!(request.headers()[AUTHORIZATION], "Bearer model-key");
assert!(!request.headers().contains_key("proxy-authorization"));
Ok(response)
})
.await
.unwrap();
assert_eq!(
socket.next().await.unwrap().unwrap().into_text().unwrap(),
"request"
);
socket
.send(Message::Text("secure response".into()))
.await
.unwrap();
let _ = socket.next().await;
} else {
let request = headers(&mut stream).await;
assert!(request.starts_with("get /responses http/1.1\r\n"));
assert!(request.contains("\r\nauthorization: bearer model-key\r\n"));
assert!(!request.contains("\r\nproxy-authorization:"));
stream
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK")
.await
.unwrap();
stream.shutdown().await.unwrap();
}
});
// Trust is scoped to this client. The production policy still uses platform roots;
// no process environment, OS trust store, hostname or chain verification is disabled.
let mut builder = policy.client_builder();
if trust != Trust::Untrusted {
builder = builder.tls_certs_only([Certificate::from_pem(CA).unwrap()]);
}
let url = format!("https://{destination}/responses");
if websocket {
let result = connect_websocket(
&policy,
builder,
&url,
HeaderMap::from_iter([(AUTHORIZATION, HeaderValue::from_static("Bearer model-key"))]),
1024,
)
.await;
if trust == Trust::Valid {
let mut socket = result.unwrap();
socket.send(Message::Text("request".into())).await.unwrap();
assert_eq!(
socket.next().await.unwrap().unwrap().into_text().unwrap(),
"secure response"
);
drop(socket);
} else {
assert!(result.is_err());
}
} else {
let result = builder
.build()
.unwrap()
.get(url)
.bearer_auth("model-key")
.send()
.await;
if trust == Trust::Valid {
assert_eq!(result.unwrap().text().await.unwrap(), "OK");
} else {
assert!(result.is_err());
}
}
server.await.unwrap();
}
#[tokio::test]
async fn connect_and_nested_tls_verify_proxy_and_origin_without_credential_leakage() {
tokio::time::timeout(Duration::from_secs(30), async {
for protocol in [ProxyProtocol::Http, ProxyProtocol::Https] {
for websocket in [false, true] {
for trust in [Trust::Valid, Trust::Untrusted, Trust::WrongName] {
exchange(protocol, websocket, trust).await;
}
}
}
})
.await
.expect("TLS proxy acceptance must settle");
}