refactor(workspace): enforce layered crate boundaries

This commit is contained in:
elky
2026-07-15 23:47:19 +08:00
parent a728c090a9
commit 8616fe6ee2
969 changed files with 40187 additions and 27240 deletions
@@ -0,0 +1,110 @@
use aether_admission_core::{
AdmissionDecision, AdmissionPolicy, AdmissionRejectReason, AdmissionRequest,
DefaultAdmissionPolicy, ResourceClass,
};
use crate::{DEFAULT_OWNER_RELAY_BODY_LIMIT_BYTES, DEFAULT_TUNNEL_PROBE_BODY_LIMIT_BYTES};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TunnelAdmissionClass {
Connection,
Relay { streaming: bool },
Probe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TunnelAdmissionRequest<'a> {
pub trace_id: &'a str,
pub class: TunnelAdmissionClass,
pub body_bytes: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TunnelAdmissionPolicy {
policy: DefaultAdmissionPolicy,
}
impl TunnelAdmissionPolicy {
pub fn decide(&self, request: TunnelAdmissionRequest<'_>) -> AdmissionDecision {
let body_limit = body_limit(request.class);
if request.body_bytes > body_limit {
return AdmissionDecision::Reject(AdmissionRejectReason::BodyTooLarge);
}
match self.policy.decide(AdmissionRequest {
trace_id: request.trace_id,
class: resource_class(request.class),
body_bytes: request.body_bytes,
}) {
AdmissionDecision::Admit(mut budget) => {
budget.body_bytes = body_limit;
AdmissionDecision::Admit(budget)
}
rejected => rejected,
}
}
}
const fn body_limit(class: TunnelAdmissionClass) -> usize {
match class {
TunnelAdmissionClass::Connection => 0,
TunnelAdmissionClass::Relay { .. } => DEFAULT_OWNER_RELAY_BODY_LIMIT_BYTES,
TunnelAdmissionClass::Probe => DEFAULT_TUNNEL_PROBE_BODY_LIMIT_BYTES,
}
}
const fn resource_class(class: TunnelAdmissionClass) -> ResourceClass {
match class {
TunnelAdmissionClass::Connection => ResourceClass::Streaming,
TunnelAdmissionClass::Relay { streaming: true } => ResourceClass::Streaming,
TunnelAdmissionClass::Relay { streaming: false } | TunnelAdmissionClass::Probe => {
ResourceClass::Interactive
}
}
}
#[cfg(test)]
mod tests {
use super::{TunnelAdmissionClass, TunnelAdmissionPolicy, TunnelAdmissionRequest};
use aether_admission_core::{AdmissionDecision, AdmissionRejectReason, DbClass};
use crate::DEFAULT_OWNER_RELAY_BODY_LIMIT_BYTES;
#[test]
fn stream_connection_reserves_stream_and_upstream_permits() {
let decision = TunnelAdmissionPolicy::default().decide(TunnelAdmissionRequest {
trace_id: "trace-1",
class: TunnelAdmissionClass::Connection,
body_bytes: 0,
});
let AdmissionDecision::Admit(budget) = decision else {
panic!("connection should be admitted");
};
assert_eq!(budget.stream_permits, 1);
assert_eq!(budget.upstream_permits, 1);
assert_eq!(budget.db_class, DbClass::ForegroundWrite);
}
#[test]
fn relay_budget_exposes_and_enforces_body_limit() {
let policy = TunnelAdmissionPolicy::default();
let decision = policy.decide(TunnelAdmissionRequest {
trace_id: "trace-2",
class: TunnelAdmissionClass::Relay { streaming: false },
body_bytes: DEFAULT_OWNER_RELAY_BODY_LIMIT_BYTES,
});
let AdmissionDecision::Admit(budget) = decision else {
panic!("relay at the limit should be admitted");
};
assert_eq!(budget.body_bytes, DEFAULT_OWNER_RELAY_BODY_LIMIT_BYTES);
assert_eq!(
policy.decide(TunnelAdmissionRequest {
trace_id: "trace-3",
class: TunnelAdmissionClass::Relay { streaming: false },
body_bytes: DEFAULT_OWNER_RELAY_BODY_LIMIT_BYTES + 1,
}),
AdmissionDecision::Reject(AdmissionRejectReason::BodyTooLarge)
);
}
}
@@ -0,0 +1,36 @@
pub mod protocol;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EmbeddedTunnelDefaults {
pub proxy_idle_timeout: Duration,
pub ping_interval: Duration,
pub max_streams: usize,
pub outbound_queue_capacity: usize,
}
impl Default for EmbeddedTunnelDefaults {
fn default() -> Self {
Self {
proxy_idle_timeout: Duration::ZERO,
ping_interval: Duration::from_secs(15),
max_streams: crate::MAX_TUNNEL_STREAMS,
outbound_queue_capacity: 512,
}
}
}
#[cfg(test)]
mod tests {
use super::EmbeddedTunnelDefaults;
#[test]
fn defaults_are_bounded_for_embedded_runtime() {
let defaults = EmbeddedTunnelDefaults::default();
assert_eq!(defaults.max_streams, 2_048);
assert_eq!(defaults.outbound_queue_capacity, 512);
assert!(defaults.proxy_idle_timeout.is_zero());
assert_eq!(defaults.ping_interval.as_secs(), 15);
}
}
@@ -0,0 +1,48 @@
//! Compatibility helpers used by the embedded gateway tunnel.
use bytes::Bytes;
pub use aether_contracts::tunnel::{
decode_payload, encode_connection_close, encode_frame, encode_goaway, encode_goaway_v3,
encode_hello, encode_load_report, encode_ping, encode_pong, encode_reset_stream,
encode_settings, encode_stream_error, encode_window_update, frame_payload_by_header,
ConnectionClosePayload, FrameHeader, GoAwayPayload, HelloPayload, LoadReportPayload,
RequestMeta, ResetStreamPayload, ResponseMeta, SettingsPayload, WindowUpdatePayload,
CONNECTION_CLOSE, FLAG_END_STREAM, FLAG_GZIP_COMPRESSED, GOAWAY, HEADER_SIZE, HEARTBEAT_ACK,
HEARTBEAT_DATA, HELLO, LOAD_REPORT, PING, PONG, REQUEST_BODY, REQUEST_HEADERS, RESET_STREAM,
RESPONSE_BODY, RESPONSE_HEADERS, SETTINGS, STREAM_END, STREAM_ERROR, WINDOW_UPDATE,
};
pub fn compress_payload(payload: &[u8]) -> Result<(Vec<u8>, u8), std::io::Error> {
let (compressed, flags) =
aether_contracts::tunnel::compress_payload(Bytes::copy_from_slice(payload));
Ok((compressed.to_vec(), flags))
}
pub fn raw_payload(payload: &[u8]) -> (Vec<u8>, u8) {
let (payload, flags) = aether_contracts::tunnel::raw_payload(Bytes::copy_from_slice(payload));
(payload.to_vec(), flags)
}
#[cfg(test)]
mod tests {
use super::{
compress_payload, decode_payload, encode_frame, raw_payload, FrameHeader,
FLAG_GZIP_COMPRESSED, RESPONSE_BODY,
};
#[test]
fn vec_compatibility_helpers_round_trip_payloads() {
let input = vec![b'a'; 4_096];
let (compressed, flags) = compress_payload(&input).expect("payload should compress");
assert_eq!(flags & FLAG_GZIP_COMPRESSED, FLAG_GZIP_COMPRESSED);
let frame = encode_frame(1, RESPONSE_BODY, flags, &compressed);
let header = FrameHeader::parse(&frame).expect("frame header should parse");
let decoded = decode_payload(&frame, &header).expect("payload should decode");
assert_eq!(decoded, input);
let (raw, raw_flags) = raw_payload(&input);
assert_eq!(raw, input);
assert_eq!(raw_flags, 0);
}
}
+95
View File
@@ -0,0 +1,95 @@
use base64::Engine as _;
use http::HeaderMap;
pub const MAX_TUNNEL_STREAMS: usize = 2_048;
pub fn resolve_proxy_max_streams(headers: &HeaderMap, fallback: usize) -> usize {
headers
.get("x-tunnel-max-streams")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(fallback)
.clamp(1, MAX_TUNNEL_STREAMS)
}
pub fn resolve_proxy_node_name(headers: &HeaderMap, node_id: &str) -> String {
if let Some(decoded) = headers
.get(aether_contracts::tunnel::TUNNEL_NODE_NAME_B64_HEADER)
.and_then(|value| value.to_str().ok())
.and_then(|value| {
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(value.trim())
.ok()
})
.and_then(|bytes| String::from_utf8(bytes).ok())
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty() && value.chars().count() <= 100)
{
return decoded;
}
headers
.get("x-node-name")
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(node_id)
.to_string()
}
pub fn resolve_proxy_protocol_version(headers: &HeaderMap) -> u8 {
headers
.get(aether_contracts::tunnel::TUNNEL_PROTOCOL_VERSION_HEADER)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<u8>().ok())
.filter(|value| *value >= 1)
.map(|value| value.min(aether_contracts::tunnel::CURRENT_TUNNEL_PROTOCOL_VERSION))
.unwrap_or(1)
}
#[cfg(test)]
mod tests {
use super::{
resolve_proxy_max_streams, resolve_proxy_node_name, resolve_proxy_protocol_version,
};
use base64::Engine as _;
use http::{HeaderMap, HeaderValue};
#[test]
fn resolves_bounded_proxy_capacity() {
let mut headers = HeaderMap::new();
headers.insert("x-tunnel-max-streams", HeaderValue::from_static("8"));
assert_eq!(resolve_proxy_max_streams(&headers, 128), 8);
headers.insert("x-tunnel-max-streams", HeaderValue::from_static("9999"));
assert_eq!(resolve_proxy_max_streams(&headers, 128), 2_048);
}
#[test]
fn resolves_protocol_version_with_v1_fallback() {
let mut headers = HeaderMap::new();
assert_eq!(resolve_proxy_protocol_version(&headers), 1);
headers.insert(
aether_contracts::tunnel::TUNNEL_PROTOCOL_VERSION_HEADER,
HeaderValue::from_static("2"),
);
assert_eq!(resolve_proxy_protocol_version(&headers), 2);
}
#[test]
fn resolves_utf8_node_name_with_legacy_fallback() {
let mut headers = HeaderMap::new();
let utf8_name = "\u{65e5}\u{672c}\u{8282}\u{70b9}";
let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(utf8_name);
headers.insert(
aether_contracts::tunnel::TUNNEL_NODE_NAME_B64_HEADER,
HeaderValue::from_str(&encoded).expect("encoded header value should parse"),
);
assert_eq!(resolve_proxy_node_name(&headers, "node-1"), utf8_name);
headers.remove(aether_contracts::tunnel::TUNNEL_NODE_NAME_B64_HEADER);
headers.insert("x-node-name", HeaderValue::from_static("edge-1"));
assert_eq!(resolve_proxy_node_name(&headers, "node-1"), "edge-1");
}
}
+23
View File
@@ -0,0 +1,23 @@
//! Shared tunnel request-lifecycle contracts.
//!
//! The executable tunnel agent and the embedded gateway tunnel both depend on
//! this crate. Network runtimes, persistence, and HTTP handlers remain in
//! their respective adapter crates.
pub mod admission;
pub mod embedded;
pub mod hub;
pub mod protocol;
pub mod relay;
pub use admission::{TunnelAdmissionClass, TunnelAdmissionPolicy, TunnelAdmissionRequest};
pub use embedded::EmbeddedTunnelDefaults;
pub use hub::{
resolve_proxy_max_streams, resolve_proxy_node_name, resolve_proxy_protocol_version,
MAX_TUNNEL_STREAMS,
};
pub use relay::{
is_tunnel_heartbeat_path, is_tunnel_node_status_path, TunnelAttachmentRecord,
DEFAULT_OWNER_RELAY_BODY_LIMIT_BYTES, DEFAULT_TUNNEL_PROBE_BODY_LIMIT_BYTES, PROXY_TUNNEL_PATH,
TUNNEL_HEARTBEAT_PATH, TUNNEL_NODE_STATUS_PATH, TUNNEL_RELAY_PATH_PATTERN, TUNNEL_ROUTE_FAMILY,
};
@@ -0,0 +1,3 @@
//! Canonical tunnel wire protocol exports shared by both runtimes.
pub use aether_contracts::tunnel::*;
+69
View File
@@ -0,0 +1,69 @@
use serde::{Deserialize, Serialize};
pub const PROXY_TUNNEL_PATH: &str = "/api/internal/proxy-tunnel";
pub const TUNNEL_HEARTBEAT_PATH: &str = "/api/internal/tunnel/heartbeat";
pub const TUNNEL_NODE_STATUS_PATH: &str = "/api/internal/tunnel/node-status";
pub const TUNNEL_RELAY_PATH_PATTERN: &str = "/api/internal/tunnel/relay/{node_id}";
pub const TUNNEL_ROUTE_FAMILY: &str = "tunnel_manage";
pub const DEFAULT_OWNER_RELAY_BODY_LIMIT_BYTES: usize = 5_242_880;
pub const DEFAULT_TUNNEL_PROBE_BODY_LIMIT_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TunnelAttachmentRecord {
pub gateway_instance_id: String,
pub relay_base_url: String,
pub conn_count: usize,
pub observed_at_unix_secs: u64,
}
impl TunnelAttachmentRecord {
pub fn is_routable(&self, now_unix_secs: u64, ttl_secs: u64) -> bool {
self.conn_count > 0
&& !self.relay_base_url.trim().is_empty()
&& self.observed_at_unix_secs.saturating_add(ttl_secs) >= now_unix_secs
}
pub fn is_owned_by(&self, gateway_instance_id: &str) -> bool {
self.gateway_instance_id == gateway_instance_id
}
}
pub fn is_tunnel_heartbeat_path(path: &str) -> bool {
path == TUNNEL_HEARTBEAT_PATH
}
pub fn is_tunnel_node_status_path(path: &str) -> bool {
path == TUNNEL_NODE_STATUS_PATH
}
#[cfg(test)]
mod tests {
use super::TunnelAttachmentRecord;
fn record() -> TunnelAttachmentRecord {
TunnelAttachmentRecord {
gateway_instance_id: "gateway-a".to_string(),
relay_base_url: "http://gateway-a.internal".to_string(),
conn_count: 1,
observed_at_unix_secs: 100,
}
}
#[test]
fn attachment_is_routable_until_ttl_boundary() {
let record = record();
assert!(record.is_routable(190, 90));
assert!(!record.is_routable(191, 90));
}
#[test]
fn attachment_requires_connection_and_relay_url() {
let mut record = record();
record.conn_count = 0;
assert!(!record.is_routable(100, 90));
record.conn_count = 1;
record.relay_base_url.clear();
assert!(!record.is_routable(100, 90));
}
}