zlayer-proxy 0.13.0

High-performance reverse proxy with TLS termination and L4/L7 routing
Documentation
//! Configuration types for stream (L4) proxy

use serde::{Deserialize, Serialize};
use std::time::Duration;

/// Configuration for a TCP listener
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TcpListenerConfig {
    /// Listen port (proxy binds to 0.0.0.0:{port})
    pub port: u16,

    /// Protocol hint for metrics/logging (e.g., "postgresql", "mongodb", "minecraft")
    #[serde(default)]
    pub protocol_hint: Option<String>,

    /// Enable TLS termination (auto-provision cert)
    #[serde(default)]
    pub tls: bool,

    /// Enable PROXY protocol for passing client IP to backend
    #[serde(default)]
    pub proxy_protocol: bool,
}

/// Configuration for a UDP listener
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UdpListenerConfig {
    /// Listen port
    pub port: u16,

    /// Protocol hint for metrics/logging (e.g., "source-engine", "game-generic")
    #[serde(default)]
    pub protocol_hint: Option<String>,

    /// Session timeout override (default uses global `udp_session_timeout`)
    #[serde(default, with = "optional_duration_serde")]
    pub session_timeout: Option<Duration>,
}

/// Default UDP session timeout (60 seconds)
pub const DEFAULT_UDP_SESSION_TIMEOUT: Duration = Duration::from_secs(60);

/// A decoded L4 health probe carried on a [`StreamProxyConfig`].
///
/// This is the runtime, proxy-local representation of the controlling
/// `StreamHealthCheck` spec type (which lives in `zlayer-types`). The agent is
/// responsible for translating the spec type into this enum (decoding hex
/// escapes in the UDP request/expect payloads, etc.) before handing it to the
/// proxy. The proxy itself never depends on `zlayer-types`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StreamHealthProbe {
    /// TCP connect probe — a successful `connect()` marks the backend healthy.
    TcpConnect,
    /// UDP probe — send `request`, mark healthy iff a reply arrives. When
    /// `expect` is `Some`, the reply must additionally contain `expect` as a
    /// (byte) substring.
    UdpProbe {
        /// Raw request payload to send to the backend.
        request: Vec<u8>,
        /// Optional expected substring in the reply.
        expect: Option<Vec<u8>>,
    },
}

/// Runtime configuration for a single L4 stream listener.
///
/// This is the proxy-local mirror of the `StreamEndpointConfig` spec type
/// (declared in `zlayer-types`). Keeping a separate type here lets the proxy
/// crate stay a leaf (no dependency on `zlayer-types`); the agent translates
/// the spec type into this one when constructing listeners.
#[derive(Debug, Clone, Default)]
pub struct StreamProxyConfig {
    /// Terminate TLS at the proxy (TCP only). When set, the listener performs
    /// the TLS handshake using the shared SNI cert resolver and relays the
    /// decrypted plaintext to the backend.
    pub tls: bool,

    /// Prepend a PROXY protocol v2 header to the upstream connection (TCP
    /// only) so the backend can recover the real client address.
    pub proxy_protocol: bool,

    /// Per-listener session timeout override (UDP only).
    pub session_timeout: Option<Duration>,

    /// Optional decoded health probe applied to this service's backends.
    pub health_check: Option<StreamHealthProbe>,
}

/// Serde helper for optional Duration serialization
mod optional_duration_serde {
    use serde::{Deserialize, Deserializer, Serialize, Serializer};
    use std::time::Duration;

    #[allow(clippy::ref_option)]
    pub fn serialize<S>(value: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match value {
            Some(d) => d.as_secs().serialize(serializer),
            None => serializer.serialize_none(),
        }
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let opt: Option<u64> = Option::deserialize(deserializer)?;
        Ok(opt.map(Duration::from_secs))
    }
}