media: let synthetic probe use mTLS

This commit is contained in:
Brad Stein 2026-05-16 22:03:12 -03:00
parent e2c86ea4c4
commit 3fd1c349a9
10 changed files with 127 additions and 13 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.22.49"
version = "0.22.50"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.22.49"
version = "0.22.50"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.22.49"
version = "0.22.50"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.22.49"
version = "0.22.50"
edition = "2024"
[dependencies]

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.22.49"
version = "0.22.50"
edition = "2024"
build = "build.rs"

View File

@ -254,6 +254,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
| `LESAVKA_TLS_DOMAIN` | client transport SNI/domain override when dialing by IP |
| `LESAVKA_TLS_KEY` | server TLS private-key path override |
| `LESAVKA_TLS_SAN` | server installer extra certificate SAN list for additional relay hostnames/IPs |
| `LESAVKA_SYNTHETIC_PKI_DIR` | server installer destination for the SSH user's local synthetic-probe mTLS identity; defaults to `~/.config/lesavka/pki` |
| `LESAVKA_UAC_BUFFER_TIME_US` | server audio sink latency override |
| `LESAVKA_UAC_COMPENSATION_US` | server audio sink latency override |
| `LESAVKA_UAC_DEV` | server hardware/device override |

View File

@ -84,6 +84,7 @@ INSTALL_UVC_FRAME_META_LOG_PATH=${LESAVKA_INSTALL_UVC_FRAME_META_LOG_PATH:-${LES
INSTALL_SERVER_BIND_ADDR=${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}
LESAVKA_TLS_DIR=${LESAVKA_TLS_DIR:-/etc/lesavka/pki}
LESAVKA_CLIENT_BUNDLE=${LESAVKA_CLIENT_BUNDLE:-/etc/lesavka/lesavka-client-pki.tar.gz}
LESAVKA_SYNTHETIC_PKI_DIR=${LESAVKA_SYNTHETIC_PKI_DIR:-$USER_HOME/.config/lesavka/pki}
LESAVKA_PASTE_KEY_FILE=${LESAVKA_PASTE_KEY_FILE:-/etc/lesavka/paste-key}
DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0
DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=135090
@ -505,6 +506,13 @@ ensure_server_tls_pki() {
sudo tar -C "$bundle_tmp" -czf "$LESAVKA_CLIENT_BUNDLE" ca.crt client.crt client.key paste-key
sudo chown "$ORIG_USER":"$ORIG_USER" "$LESAVKA_CLIENT_BUNDLE"
sudo chmod 0600 "$LESAVKA_CLIENT_BUNDLE"
if [[ -n $USER_HOME && $ORIG_USER != root ]]; then
sudo install -d -m 0700 -o "$ORIG_USER" -g "$ORIG_USER" "$LESAVKA_SYNTHETIC_PKI_DIR"
sudo install -m 0644 -o "$ORIG_USER" -g "$ORIG_USER" "$LESAVKA_TLS_DIR/ca.crt" "$LESAVKA_SYNTHETIC_PKI_DIR/ca.crt"
sudo install -m 0644 -o "$ORIG_USER" -g "$ORIG_USER" "$LESAVKA_TLS_DIR/client.crt" "$LESAVKA_SYNTHETIC_PKI_DIR/client.crt"
sudo install -m 0600 -o "$ORIG_USER" -g "$ORIG_USER" "$LESAVKA_TLS_DIR/client.key" "$LESAVKA_SYNTHETIC_PKI_DIR/client.key"
echo " ↪ local synthetic probe TLS identity: $LESAVKA_SYNTHETIC_PKI_DIR"
fi
sudo rm -rf "$bundle_tmp"
echo " ↪ client enrollment bundle: $LESAVKA_CLIENT_BUNDLE"
}

View File

@ -31,7 +31,7 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument("--inject-host", default="", help="Theia SSH host, e.g. titan-jh")
parser.add_argument("--rct-host", default="", help="RCT SSH host, e.g. tethys")
parser.add_argument("--server", default="http://127.0.0.1:50051")
parser.add_argument("--server", default="https://127.0.0.1:50051")
parser.add_argument("--inject-binary", default="/usr/local/bin/lesavka-synthetic-uplink")
parser.add_argument("--mode", default="1280x720@30", help=f"one mode; baseline set is {DEFAULT_MODES}")
parser.add_argument("--width", type=int, default=0, help="override capture width")

View File

@ -16,7 +16,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.22.49"
version = "0.22.50"
edition = "2024"
autobins = false

View File

@ -12,6 +12,7 @@ use lesavka_common::lesavka::{
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::Request;
use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity};
const DEFAULT_SERVER: &str = "http://127.0.0.1:50051";
const DEFAULT_SAMPLE_RATE: u32 = 48_000;
@ -31,6 +32,10 @@ struct Args {
session_id: u64,
artifact_dir: Option<PathBuf>,
print_every: u64,
tls_ca: Option<PathBuf>,
tls_client_cert: Option<PathBuf>,
tls_client_key: Option<PathBuf>,
tls_domain: Option<String>,
}
impl Args {
@ -45,6 +50,14 @@ impl Args {
session_id: unix_millis(),
artifact_dir: None,
print_every: 150,
tls_ca: env_path("LESAVKA_TLS_CA").or_else(|| default_pki_path("ca.crt")),
tls_client_cert: env_path("LESAVKA_TLS_CLIENT_CERT")
.or_else(|| default_pki_path("client.crt")),
tls_client_key: env_path("LESAVKA_TLS_CLIENT_KEY")
.or_else(|| default_pki_path("client.key")),
tls_domain: std::env::var("LESAVKA_TLS_DOMAIN")
.ok()
.filter(|value| !value.trim().is_empty()),
};
let mut it = std::env::args().skip(1);
while let Some(flag) = it.next() {
@ -63,6 +76,14 @@ impl Args {
args.artifact_dir = Some(PathBuf::from(next_value(&mut it, &flag)?))
}
"--print-every" => args.print_every = parse_next(&mut it, &flag)?,
"--tls-ca" => args.tls_ca = Some(PathBuf::from(next_value(&mut it, &flag)?)),
"--tls-client-cert" => {
args.tls_client_cert = Some(PathBuf::from(next_value(&mut it, &flag)?))
}
"--tls-client-key" => {
args.tls_client_key = Some(PathBuf::from(next_value(&mut it, &flag)?))
}
"--tls-domain" => args.tls_domain = Some(next_value(&mut it, &flag)?),
"--mode" => {
let value = next_value(&mut it, &flag)?;
let (width, height, fps) = parse_mode(&value)?;
@ -207,6 +228,85 @@ impl Drop for MjpegEncoder {
}
}
async fn connect_channel(args: &Args) -> Result<Channel> {
let mut endpoint =
tonic::transport::Channel::from_shared(args.server.clone())?.tcp_nodelay(true);
if is_https(&args.server) {
endpoint = endpoint
.tls_config(client_tls_config(args)?)
.context("configuring synthetic uplink TLS")?;
}
endpoint
.connect()
.await
.with_context(|| format!("connecting to {}", args.server))
}
fn client_tls_config(args: &Args) -> Result<ClientTlsConfig> {
let mut tls = ClientTlsConfig::new().domain_name(
args.tls_domain
.clone()
.or_else(|| host_from_uri(&args.server))
.unwrap_or_else(|| "lesavka-server".to_string()),
);
let ca_path = args
.tls_ca
.as_ref()
.context("https synthetic uplink requires --tls-ca or LESAVKA_TLS_CA")?;
let cert_path = args
.tls_client_cert
.as_ref()
.context("https synthetic uplink requires --tls-client-cert or LESAVKA_TLS_CLIENT_CERT")?;
let key_path = args
.tls_client_key
.as_ref()
.context("https synthetic uplink requires --tls-client-key or LESAVKA_TLS_CLIENT_KEY")?;
let ca = std::fs::read(ca_path)
.with_context(|| format!("reading TLS CA certificate {}", ca_path.display()))?;
tls = tls.ca_certificate(Certificate::from_pem(ca));
let cert = std::fs::read(cert_path)
.with_context(|| format!("reading TLS client certificate {}", cert_path.display()))?;
let key = std::fs::read(key_path)
.with_context(|| format!("reading TLS client key {}", key_path.display()))?;
Ok(tls.identity(Identity::from_pem(cert, key)))
}
fn is_https(server: &str) -> bool {
server.trim_start().starts_with("https://")
}
fn host_from_uri(server: &str) -> Option<String> {
let rest = server.split_once("://")?.1;
let host_port = rest.split('/').next().unwrap_or(rest);
let host = host_port
.rsplit_once('@')
.map(|(_, host)| host)
.unwrap_or(host_port);
if host.starts_with('[') {
return host
.split_once(']')
.map(|(value, _)| value.trim_start_matches('[').to_string());
}
Some(host.split(':').next().unwrap_or(host).to_string()).filter(|host| !host.is_empty())
}
fn env_path(name: &str) -> Option<PathBuf> {
std::env::var_os(name)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
fn default_pki_path(file_name: &str) -> Option<PathBuf> {
let home = std::env::var_os("HOME")?;
Some(
PathBuf::from(home)
.join(".config")
.join("lesavka")
.join("pki")
.join(file_name),
)
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let args = Args::parse()?;
@ -219,11 +319,7 @@ async fn main() -> Result<()> {
std::fs::write(dir.join("summary.json"), args_summary_json(&args) + "\n")?;
}
let channel = tonic::transport::Channel::from_shared(args.server.clone())?
.tcp_nodelay(true)
.connect()
.await
.with_context(|| format!("connecting to {}", args.server))?;
let channel = connect_channel(&args).await?;
let mut client = RelayClient::new(channel);
let (tx, rx) = mpsc::channel::<UpstreamMediaBundle>(8);
let response_task = tokio::spawn(async move {
@ -484,13 +580,14 @@ fn unix_millis() -> u64 {
fn args_summary_json(args: &Args) -> String {
format!(
"{{\"schema\":\"lesavka.synthetic-uplink.v1\",\"server\":{server:?},\"width\":{width},\"height\":{height},\"fps\":{fps},\"duration_s\":{duration:.3},\"session_id\":{session}}}",
"{{\"schema\":\"lesavka.synthetic-uplink.v1\",\"server\":{server:?},\"width\":{width},\"height\":{height},\"fps\":{fps},\"duration_s\":{duration:.3},\"session_id\":{session},\"tls\":{tls}}}",
server = args.server,
width = args.width,
height = args.height,
fps = args.fps,
duration = args.duration.as_secs_f64(),
session = args.session_id,
tls = is_https(&args.server),
)
}

View File

@ -56,6 +56,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
"LESAVKA_TLS_CERT=%s",
"LESAVKA_TLS_KEY=%s",
"LESAVKA_TLS_CLIENT_CA=%s",
"LESAVKA_SYNTHETIC_PKI_DIR",
"LESAVKA_PASTE_KEY_FILE=%s",
] {
assert!(
@ -708,6 +709,8 @@ fn server_install_generates_mtls_identity_and_client_bundle() {
"Client TLS bundle:",
"Client install can use:",
"sudo chown \"$ORIG_USER\":\"$ORIG_USER\" \"$LESAVKA_CLIENT_BUNDLE\"",
"local synthetic probe TLS identity",
"$USER_HOME/.config/lesavka/pki",
"LESAVKA_REQUIRE_TLS=%s",
"LESAVKA_TLS_CLIENT_CA=%s",
] {

View File

@ -33,6 +33,7 @@ fn synthetic_probe_keeps_bundled_network_ingress_and_rct_comparison_markers() {
"lesavka.synthetic-rct-probe.orchestrator.v1",
"StreamWebcamMedia",
"lesavka-synthetic-uplink",
"https://127.0.0.1:50051",
"--inject-host",
"--rct-host",
"--capture-only",
@ -80,6 +81,10 @@ fn synthetic_injector_enters_the_public_bundled_media_rpc() {
"jpegenc",
"client_capture_pts_us: pts_us",
"client_send_pts_us: pts_us",
"ClientTlsConfig",
"--tls-ca",
"--tls-client-cert",
"--tls-client-key",
"StreamWebcamMedia closed before accepting synthetic frame",
] {
assert!(