use serde::{Deserialize, Serialize};
use crate::mesh::discovery_policy::{
DiscoveryConsent, DiscoveryMode, RespondDecisionInput, decide_respond,
};
pub const HELLO_REQUEST_SCHEMA_V1: &str = "ee.mesh.hello.v1";
pub const HELLO_RESPONSE_SCHEMA_V1: &str = "ee.mesh.hello.response.v1";
pub const HELLO_ERROR_SCHEMA_V1: &str = "ee.mesh.hello.error.v1";
pub const HELLO_PAYLOAD_BUDGET_BYTES: usize = 4096;
pub const HELLO_PROTOCOL_VERSION_MAJOR: u32 = 1;
pub const HELLO_PROTOCOL_VERSION_MINOR: u32 = 0;
#[must_use]
pub fn local_protocol_version_string() -> String {
format!("{HELLO_PROTOCOL_VERSION_MAJOR}.{HELLO_PROTOCOL_VERSION_MINOR}")
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct ProtocolVersion {
pub major: u32,
pub minor: u32,
}
impl ProtocolVersion {
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
let mut split = value.split('.');
let major_str = split.next()?;
let minor_str = split.next()?;
if split.next().is_some() {
return None;
}
let major: u32 = major_str.parse().ok()?;
let minor: u32 = minor_str.parse().ok()?;
Some(Self { major, minor })
}
#[must_use]
pub fn is_compatible_with(&self, local: Self) -> bool {
self.major == local.major
}
#[must_use]
pub fn local() -> Self {
Self {
major: HELLO_PROTOCOL_VERSION_MAJOR,
minor: HELLO_PROTOCOL_VERSION_MINOR,
}
}
}
impl std::fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HelloErrorCode {
UnsupportedProtocolVersion,
DiscoveryConsentDenied,
ResponderBusy,
ResponderMeshDisabled,
ResponderShieldsUp,
ResponderUnauthenticatedTailscale,
}
impl HelloErrorCode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::UnsupportedProtocolVersion => "unsupported_protocol_version",
Self::DiscoveryConsentDenied => "discovery_consent_denied",
Self::ResponderBusy => "responder_busy",
Self::ResponderMeshDisabled => "responder_mesh_disabled",
Self::ResponderShieldsUp => "responder_shields_up",
Self::ResponderUnauthenticatedTailscale => "responder_unauthenticated_tailscale",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct HelloRequest {
pub schema: &'static str,
#[serde(rename = "requestId")]
pub request_id: String,
#[serde(rename = "requesterNodeKey")]
pub requester_node_key: String,
#[serde(rename = "requesterEeVersion")]
pub requester_ee_version: String,
#[serde(rename = "requesterEeProtocolVersion")]
pub requester_ee_protocol_version: String,
#[serde(rename = "requesterWorkspaceIds", default)]
pub requester_workspace_ids: Vec<String>,
#[serde(rename = "requesterCapabilities", default)]
pub requester_capabilities: Vec<String>,
#[serde(
rename = "requesterAdvertisedTags",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub requester_advertised_tags: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct HelloResponse {
pub schema: &'static str,
#[serde(rename = "requestId")]
pub request_id: String,
#[serde(rename = "responderNodeKey")]
pub responder_node_key: String,
#[serde(rename = "responderEeVersion")]
pub responder_ee_version: String,
#[serde(rename = "responderEeProtocolVersion")]
pub responder_ee_protocol_version: String,
#[serde(rename = "responderWorkspaceIds", default)]
pub responder_workspace_ids: Vec<String>,
#[serde(rename = "responderCapabilities", default)]
pub responder_capabilities: Vec<String>,
#[serde(
rename = "responderAdvertisedTags",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub responder_advertised_tags: Vec<String>,
#[serde(rename = "discoveryConsent")]
pub discovery_consent: bool,
#[serde(rename = "responseElapsedMicros")]
pub response_elapsed_micros: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct HelloError {
pub schema: &'static str,
#[serde(rename = "requestId")]
pub request_id: String,
#[serde(rename = "discoveryConsent")]
pub discovery_consent: bool,
pub code: HelloErrorCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[must_use]
pub fn build_request(
request_id: impl Into<String>,
requester_node_key: impl Into<String>,
requester_ee_version: impl Into<String>,
requester_workspace_ids: Vec<String>,
requester_capabilities: Vec<String>,
requester_advertised_tags: Vec<String>,
) -> HelloRequest {
HelloRequest {
schema: HELLO_REQUEST_SCHEMA_V1,
request_id: request_id.into(),
requester_node_key: requester_node_key.into(),
requester_ee_version: requester_ee_version.into(),
requester_ee_protocol_version: local_protocol_version_string(),
requester_workspace_ids,
requester_capabilities,
requester_advertised_tags,
}
}
pub fn serialize_within_budget<T: Serialize>(value: &T) -> Result<Vec<u8>, HelloSerializeError> {
let serialized = serde_json::to_vec(value).map_err(HelloSerializeError::Json)?;
if serialized.len() > HELLO_PAYLOAD_BUDGET_BYTES {
return Err(HelloSerializeError::PayloadTooLarge {
actual_bytes: serialized.len(),
budget_bytes: HELLO_PAYLOAD_BUDGET_BYTES,
});
}
Ok(serialized)
}
pub fn serialized_payload_fits_budget<T: Serialize>(value: &T) -> bool {
serde_json::to_vec(value)
.map(|bytes| bytes.len() <= HELLO_PAYLOAD_BUDGET_BYTES)
.unwrap_or(false)
}
#[derive(Debug)]
pub enum HelloSerializeError {
Json(serde_json::Error),
PayloadTooLarge {
actual_bytes: usize,
budget_bytes: usize,
},
}
impl std::fmt::Display for HelloSerializeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Json(error) => write!(f, "failed to serialize hello payload: {error}"),
Self::PayloadTooLarge {
actual_bytes,
budget_bytes,
} => write!(
f,
"hello payload {actual_bytes} bytes exceeds {budget_bytes}-byte budget"
),
}
}
}
impl std::error::Error for HelloSerializeError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResponderContext<'a> {
pub mesh_enabled: bool,
pub tailscale_authenticated: bool,
pub shields_up: bool,
pub respond_mode: DiscoveryMode,
pub responder_node_key: &'a str,
pub responder_ee_version: &'a str,
pub responder_workspace_ids: &'a [String],
pub responder_capabilities: &'a [String],
pub responder_advertised_tags: &'a [String],
pub respond_allowlist: &'a std::collections::BTreeSet<String>,
pub denylist: &'a std::collections::BTreeSet<String>,
pub rate_limited: bool,
pub elapsed_micros: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HelloOutcome {
Granted(HelloResponse),
Declined(HelloError),
}
impl HelloOutcome {
#[must_use]
pub fn is_granted(&self) -> bool {
matches!(self, Self::Granted(_))
}
#[must_use]
pub fn response(&self) -> Option<&HelloResponse> {
if let Self::Granted(r) = self {
Some(r)
} else {
None
}
}
#[must_use]
pub fn error(&self) -> Option<&HelloError> {
if let Self::Declined(e) = self {
Some(e)
} else {
None
}
}
}
#[must_use]
pub fn decide_hello_response(request: &HelloRequest, ctx: &ResponderContext<'_>) -> HelloOutcome {
let echo = request.request_id.clone();
if ctx.rate_limited {
return decline(echo, HelloErrorCode::ResponderBusy, None);
}
if !ctx.mesh_enabled {
return decline(echo, HelloErrorCode::ResponderMeshDisabled, None);
}
if !ctx.tailscale_authenticated {
return decline(
echo,
HelloErrorCode::ResponderUnauthenticatedTailscale,
None,
);
}
if ctx.shields_up {
return decline(echo, HelloErrorCode::ResponderShieldsUp, None);
}
let local = ProtocolVersion::local();
let requester_version = ProtocolVersion::parse(&request.requester_ee_protocol_version);
let compatible = requester_version
.map(|v| v.is_compatible_with(local))
.unwrap_or(false);
if !compatible {
return decline(
echo,
HelloErrorCode::UnsupportedProtocolVersion,
Some(format!("requires major {HELLO_PROTOCOL_VERSION_MAJOR}.x")),
);
}
let (consent, _reason) = decide_respond(&RespondDecisionInput {
mode: ctx.respond_mode,
requester_node_key: &request.requester_node_key,
requester_advertised_tags: &request.requester_advertised_tags,
self_advertised_tags: ctx.responder_advertised_tags,
respond_allowlist: ctx.respond_allowlist,
denylist: ctx.denylist,
});
match consent {
DiscoveryConsent::Denied => decline(echo, HelloErrorCode::DiscoveryConsentDenied, None),
DiscoveryConsent::Granted => {
tracing::debug!(
target: "ee::mesh::hello",
request_id = %echo,
requester_node_key = %request.requester_node_key,
"hello granted"
);
HelloOutcome::Granted(HelloResponse {
schema: HELLO_RESPONSE_SCHEMA_V1,
request_id: echo,
responder_node_key: ctx.responder_node_key.to_owned(),
responder_ee_version: ctx.responder_ee_version.to_owned(),
responder_ee_protocol_version: local_protocol_version_string(),
responder_workspace_ids: ctx.responder_workspace_ids.to_vec(),
responder_capabilities: ctx.responder_capabilities.to_vec(),
responder_advertised_tags: ctx.responder_advertised_tags.to_vec(),
discovery_consent: true,
response_elapsed_micros: ctx.elapsed_micros,
})
}
}
}
#[inline]
fn decline(echo: String, code: HelloErrorCode, detail: Option<String>) -> HelloOutcome {
HelloOutcome::Declined(HelloError {
schema: HELLO_ERROR_SCHEMA_V1,
request_id: echo,
discovery_consent: false,
code,
detail,
})
}
#[must_use]
pub fn classify_decline_for_caller_skip_reason(code: HelloErrorCode) -> &'static str {
match code {
HelloErrorCode::UnsupportedProtocolVersion => "probe_malformed",
HelloErrorCode::DiscoveryConsentDenied => "no_discovery_consent",
HelloErrorCode::ResponderBusy => "probe_timeout",
HelloErrorCode::ResponderMeshDisabled => "non_ee",
HelloErrorCode::ResponderShieldsUp => "no_discovery_consent",
HelloErrorCode::ResponderUnauthenticatedTailscale => "non_ee",
}
}
pub fn assert_no_responder_metadata_leak(error: &HelloError) -> Result<(), &'static str> {
let serialized = serde_json::to_value(error).map_err(|_| "serde_serialize")?;
let object = serialized.as_object().ok_or("not_object")?;
for forbidden in [
"responderNodeKey",
"responderEeVersion",
"responderEeProtocolVersion",
"responderWorkspaceIds",
"responderCapabilities",
"responderAdvertisedTags",
"responseElapsedMicros",
] {
if object.contains_key(forbidden) {
return Err(forbidden);
}
}
Ok(())
}
fn json_string_list(value: Option<&serde_json::Value>) -> Vec<String> {
value
.and_then(serde_json::Value::as_array)
.map(|items| {
items
.iter()
.filter_map(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.collect()
})
.unwrap_or_default()
}
#[must_use]
pub fn parse_hello_request(value: &serde_json::Value) -> Option<HelloRequest> {
if value.get("schema").and_then(serde_json::Value::as_str) != Some(HELLO_REQUEST_SCHEMA_V1) {
return None;
}
Some(HelloRequest {
schema: HELLO_REQUEST_SCHEMA_V1,
request_id: value.get("requestId")?.as_str()?.to_owned(),
requester_node_key: value.get("requesterNodeKey")?.as_str()?.to_owned(),
requester_ee_version: value.get("requesterEeVersion")?.as_str()?.to_owned(),
requester_ee_protocol_version: value
.get("requesterEeProtocolVersion")?
.as_str()?
.to_owned(),
requester_workspace_ids: json_string_list(value.get("requesterWorkspaceIds")),
requester_capabilities: json_string_list(value.get("requesterCapabilities")),
requester_advertised_tags: json_string_list(value.get("requesterAdvertisedTags")),
})
}
#[must_use]
pub fn parse_hello_response(value: &serde_json::Value) -> Option<HelloResponse> {
if value.get("schema").and_then(serde_json::Value::as_str) != Some(HELLO_RESPONSE_SCHEMA_V1) {
return None;
}
Some(HelloResponse {
schema: HELLO_RESPONSE_SCHEMA_V1,
request_id: value.get("requestId")?.as_str()?.to_owned(),
responder_node_key: value.get("responderNodeKey")?.as_str()?.to_owned(),
responder_ee_version: value.get("responderEeVersion")?.as_str()?.to_owned(),
responder_ee_protocol_version: value
.get("responderEeProtocolVersion")?
.as_str()?
.to_owned(),
responder_workspace_ids: json_string_list(value.get("responderWorkspaceIds")),
responder_capabilities: json_string_list(value.get("responderCapabilities")),
responder_advertised_tags: json_string_list(value.get("responderAdvertisedTags")),
discovery_consent: value.get("discoveryConsent")?.as_bool()?,
response_elapsed_micros: value.get("responseElapsedMicros")?.as_u64()?,
})
}
#[must_use]
pub fn parse_hello_error(value: &serde_json::Value) -> Option<HelloError> {
if value.get("schema").and_then(serde_json::Value::as_str) != Some(HELLO_ERROR_SCHEMA_V1) {
return None;
}
let code = serde_json::from_value(value.get("code")?.clone()).ok()?;
Some(HelloError {
schema: HELLO_ERROR_SCHEMA_V1,
request_id: value.get("requestId")?.as_str()?.to_owned(),
discovery_consent: value.get("discoveryConsent")?.as_bool()?,
code,
detail: value
.get("detail")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn empty_set() -> BTreeSet<String> {
BTreeSet::new()
}
fn fixture_request() -> HelloRequest {
HelloRequest {
schema: HELLO_REQUEST_SCHEMA_V1,
request_id: "req_alpha".to_owned(),
requester_node_key: "nodekey:caller".to_owned(),
requester_ee_version: "0.2.0".to_owned(),
requester_ee_protocol_version: local_protocol_version_string(),
requester_workspace_ids: vec!["wsp_one".to_owned()],
requester_capabilities: vec!["discovery".to_owned()],
requester_advertised_tags: vec![],
}
}
#[test]
fn parse_hello_payloads_round_trip() {
let request = fixture_request();
let value = serde_json::to_value(&request).expect("serialize request");
assert_eq!(parse_hello_request(&value).expect("parse request"), request);
let granted = match decide_hello_response(
&request,
&fixture_ctx(
"nodekey:responder",
&["wsp_one".to_owned()],
&["hello".to_owned()],
&[],
&empty_set(),
&empty_set(),
),
) {
HelloOutcome::Granted(response) => response,
HelloOutcome::Declined(error) => panic!("expected grant, got {error:?}"),
};
let granted_value = serde_json::to_value(&granted).expect("serialize response");
assert_eq!(
parse_hello_response(&granted_value).expect("parse response"),
granted
);
}
fn fixture_request_with_protocol(protocol_version: impl Into<String>) -> HelloRequest {
let mut req = fixture_request();
req.requester_ee_protocol_version = protocol_version.into();
req
}
fn fixture_ctx<'a>(
responder_node_key: &'a str,
responder_workspace_ids: &'a [String],
responder_capabilities: &'a [String],
responder_advertised_tags: &'a [String],
respond_allowlist: &'a BTreeSet<String>,
denylist: &'a BTreeSet<String>,
) -> ResponderContext<'a> {
ResponderContext {
mesh_enabled: true,
tailscale_authenticated: true,
shields_up: false,
respond_mode: DiscoveryMode::AutoAdmit,
responder_node_key,
responder_ee_version: "0.2.0",
responder_workspace_ids,
responder_capabilities,
responder_advertised_tags,
respond_allowlist,
denylist,
rate_limited: false,
elapsed_micros: 42,
}
}
#[test]
fn protocol_version_parses_valid_major_minor() {
let v = ProtocolVersion::parse("3.7").expect("ok");
assert_eq!(v, ProtocolVersion { major: 3, minor: 7 });
}
#[test]
fn protocol_version_rejects_three_segments() {
assert!(ProtocolVersion::parse("1.0.0").is_none());
}
#[test]
fn protocol_version_rejects_non_numeric_segments() {
assert!(ProtocolVersion::parse("a.b").is_none());
assert!(ProtocolVersion::parse("1.x").is_none());
assert!(ProtocolVersion::parse("x.1").is_none());
}
#[test]
fn protocol_version_rejects_empty() {
assert!(ProtocolVersion::parse("").is_none());
assert!(ProtocolVersion::parse(".").is_none());
assert!(ProtocolVersion::parse(".1").is_none());
assert!(ProtocolVersion::parse("1.").is_none());
}
#[test]
fn protocol_version_is_compatible_when_majors_match() {
let local = ProtocolVersion::local();
let same_major = ProtocolVersion {
major: local.major,
minor: local.minor.saturating_add(99),
};
assert!(same_major.is_compatible_with(local));
}
#[test]
fn protocol_version_is_incompatible_when_majors_differ() {
let local = ProtocolVersion::local();
let other_major = ProtocolVersion {
major: local.major.saturating_add(1),
minor: local.minor,
};
assert!(!other_major.is_compatible_with(local));
}
#[test]
fn protocol_version_same_major_matrix_allows_minor_skew() {
let simulated_local = ProtocolVersion {
major: 7,
minor: 10,
};
for requester in [
ProtocolVersion { major: 7, minor: 0 },
ProtocolVersion {
major: 7,
minor: 10,
},
ProtocolVersion {
major: 7,
minor: 99,
},
] {
assert!(
requester.is_compatible_with(simulated_local),
"same-major requester {requester} should negotiate with local {simulated_local}"
);
}
}
#[test]
fn protocol_version_matrix_rejects_older_and_newer_majors() {
let local = ProtocolVersion {
major: 7,
minor: 10,
};
for requester in [
ProtocolVersion {
major: 6,
minor: 99,
},
ProtocolVersion { major: 8, minor: 0 },
] {
assert!(
!requester.is_compatible_with(local),
"cross-major requester {requester} must not negotiate with local {local}"
);
}
}
#[test]
fn build_request_uses_local_protocol_version() {
let req = build_request(
"req_x",
"nodekey:me",
"0.2.0",
vec!["wsp_a".to_owned()],
vec!["discovery".to_owned()],
vec![],
);
assert_eq!(req.schema, HELLO_REQUEST_SCHEMA_V1);
assert_eq!(
req.requester_ee_protocol_version,
local_protocol_version_string()
);
assert_eq!(req.request_id, "req_x");
}
#[test]
fn build_request_serializes_to_valid_json_under_budget() {
let req = fixture_request();
let bytes = serialize_within_budget(&req).expect("under budget");
assert!(bytes.len() < HELLO_PAYLOAD_BUDGET_BYTES);
let bytes_owned: &'static [u8] = bytes.leak();
let round_trip: HelloRequest = serde_json::from_slice(bytes_owned).expect("round-trip");
assert_eq!(round_trip, req);
}
#[test]
fn serialize_within_budget_rejects_oversized_payloads() {
let mut req = fixture_request();
req.requester_workspace_ids = (0..200)
.map(|i| format!("wsp_long_workspace_id_index_{i:04}_aaaaaaaa"))
.collect();
let result = serialize_within_budget(&req);
assert!(matches!(
result,
Err(HelloSerializeError::PayloadTooLarge { .. })
));
}
#[test]
fn handler_returns_responder_busy_when_rate_limited() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let mut ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
ctx.rate_limited = true;
let outcome = decide_hello_response(&fixture_request(), &ctx);
let err = outcome.error().expect("declined");
assert_eq!(err.code, HelloErrorCode::ResponderBusy);
}
#[test]
fn handler_returns_responder_mesh_disabled_when_env_false() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let mut ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
ctx.mesh_enabled = false;
let outcome = decide_hello_response(&fixture_request(), &ctx);
let err = outcome.error().expect("declined");
assert_eq!(err.code, HelloErrorCode::ResponderMeshDisabled);
}
#[test]
fn handler_returns_responder_unauthenticated_tailscale_when_probe_says_so() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let mut ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
ctx.tailscale_authenticated = false;
let outcome = decide_hello_response(&fixture_request(), &ctx);
let err = outcome.error().expect("declined");
assert_eq!(err.code, HelloErrorCode::ResponderUnauthenticatedTailscale);
}
#[test]
fn handler_returns_responder_shields_up_when_set() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let mut ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
ctx.shields_up = true;
let outcome = decide_hello_response(&fixture_request(), &ctx);
let err = outcome.error().expect("declined");
assert_eq!(err.code, HelloErrorCode::ResponderShieldsUp);
}
#[test]
fn handler_skips_peer_on_incompatible_major_version() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
let req = fixture_request_with_protocol(format!(
"{}.{}",
HELLO_PROTOCOL_VERSION_MAJOR + 1,
HELLO_PROTOCOL_VERSION_MINOR
));
let outcome = decide_hello_response(&req, &ctx);
let err = outcome.error().expect("declined");
assert_eq!(err.code, HelloErrorCode::UnsupportedProtocolVersion);
}
#[test]
fn handler_treats_malformed_protocol_version_as_unsupported() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
let req = fixture_request_with_protocol("garbage");
let outcome = decide_hello_response(&req, &ctx);
let err = outcome.error().expect("declined");
assert_eq!(err.code, HelloErrorCode::UnsupportedProtocolVersion);
}
#[test]
fn handler_accepts_same_major_protocol_minor_skew_matrix() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let ws = vec!["wsp_one".to_owned()];
let caps = vec!["discovery".to_owned()];
let ctx = fixture_ctx("nodekey:responder", &ws, &caps, &tags, &allow, &deny);
for requester_version in [
format!("{HELLO_PROTOCOL_VERSION_MAJOR}.0"),
local_protocol_version_string(),
format!(
"{}.{}",
HELLO_PROTOCOL_VERSION_MAJOR,
HELLO_PROTOCOL_VERSION_MINOR.saturating_add(1)
),
format!("{HELLO_PROTOCOL_VERSION_MAJOR}.99"),
] {
let req = fixture_request_with_protocol(&requester_version);
let outcome = decide_hello_response(&req, &ctx);
let resp = outcome
.response()
.unwrap_or_else(|| panic!("same-major requester {requester_version} declined"));
assert_eq!(
resp.responder_ee_protocol_version,
local_protocol_version_string()
);
assert_eq!(resp.responder_capabilities, vec!["discovery"]);
}
}
#[test]
fn handler_fail_closed_matrix_rejects_cross_major_and_malformed_protocols() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
for requester_version in [
format!(
"{}.{}",
HELLO_PROTOCOL_VERSION_MAJOR.saturating_sub(1),
HELLO_PROTOCOL_VERSION_MINOR
),
format!(
"{}.{}",
HELLO_PROTOCOL_VERSION_MAJOR.saturating_add(1),
HELLO_PROTOCOL_VERSION_MINOR
),
"garbage".to_owned(),
"1.0.0".to_owned(),
] {
let req = fixture_request_with_protocol(&requester_version);
let outcome = decide_hello_response(&req, &ctx);
let err = outcome
.error()
.unwrap_or_else(|| panic!("requester {requester_version} was granted"));
assert_eq!(err.code, HelloErrorCode::UnsupportedProtocolVersion);
assert!(assert_no_responder_metadata_leak(err).is_ok());
let expected_detail = format!("requires major {HELLO_PROTOCOL_VERSION_MAJOR}.x");
assert_eq!(err.detail.as_deref(), Some(expected_detail.as_str()));
}
}
#[test]
fn handler_returns_discovery_consent_denied_when_policy_refuses() {
let allow = empty_set();
let mut deny = BTreeSet::new();
deny.insert("nodekey:caller".to_owned());
let tags = vec![];
let ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
let outcome = decide_hello_response(&fixture_request(), &ctx);
let err = outcome.error().expect("declined");
assert_eq!(err.code, HelloErrorCode::DiscoveryConsentDenied);
}
#[test]
fn handler_grants_consent_and_echoes_request_id() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let ws = vec!["wsp_one".to_owned()];
let caps = vec!["discovery".to_owned()];
let ctx = fixture_ctx("nodekey:responder", &ws, &caps, &tags, &allow, &deny);
let outcome = decide_hello_response(&fixture_request(), &ctx);
let resp = outcome.response().expect("granted");
assert_eq!(resp.schema, HELLO_RESPONSE_SCHEMA_V1);
assert_eq!(resp.request_id, "req_alpha");
assert_eq!(resp.responder_node_key, "nodekey:responder");
assert_eq!(resp.responder_workspace_ids, vec!["wsp_one"]);
assert_eq!(resp.responder_capabilities, vec!["discovery"]);
assert!(resp.discovery_consent);
assert_eq!(resp.response_elapsed_micros, 42);
assert_eq!(
resp.responder_ee_protocol_version,
local_protocol_version_string()
);
}
#[test]
fn handler_grants_consent_when_service_tag_mode_and_self_advertises_tag() {
let allow = empty_set();
let deny = empty_set();
let self_tags = vec![crate::mesh::discovery_policy::EE_MESH_SERVICE_TAG.to_owned()];
let mut ctx = fixture_ctx("nodekey:responder", &[], &[], &self_tags, &allow, &deny);
ctx.respond_mode = DiscoveryMode::ServiceTag;
let outcome = decide_hello_response(&fixture_request(), &ctx);
let resp = outcome.response().expect("granted");
assert_eq!(resp.discovery_consent, true);
}
#[test]
fn handler_denies_consent_under_service_tag_mode_without_self_tag() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let mut ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
ctx.respond_mode = DiscoveryMode::ServiceTag;
let outcome = decide_hello_response(&fixture_request(), &ctx);
let err = outcome.error().expect("declined");
assert_eq!(err.code, HelloErrorCode::DiscoveryConsentDenied);
}
#[test]
fn decline_response_omits_responder_metadata() {
let allow = empty_set();
let mut deny = BTreeSet::new();
deny.insert("nodekey:caller".to_owned());
let tags = vec![];
let ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
let outcome = decide_hello_response(&fixture_request(), &ctx);
let err = outcome.error().expect("declined");
assert!(assert_no_responder_metadata_leak(err).is_ok());
let json = serde_json::to_string(err).expect("serialize");
assert!(!json.contains("responderNodeKey"));
assert!(!json.contains("responderEeVersion"));
assert!(!json.contains("responderWorkspaceIds"));
assert!(!json.contains("responderCapabilities"));
assert!(!json.contains("responderAdvertisedTags"));
assert!(!json.contains("responseElapsedMicros"));
}
#[test]
fn decline_unsupported_protocol_version_includes_safe_detail() {
let allow = empty_set();
let deny = empty_set();
let tags = vec![];
let ctx = fixture_ctx("nodekey:responder", &[], &[], &tags, &allow, &deny);
let req = fixture_request_with_protocol("99.0");
let outcome = decide_hello_response(&req, &ctx);
let err = outcome.error().expect("declined");
let detail = err.detail.as_deref().unwrap_or("");
assert!(detail.contains(&format!("major {HELLO_PROTOCOL_VERSION_MAJOR}.x")));
assert!(!detail.contains("nodekey"));
assert!(!detail.contains("tailnet"));
}
#[test]
fn request_deserialization_tolerates_missing_optional_advertised_tags() {
let json = r#"{
"schema": "ee.mesh.hello.v1",
"requestId": "req_x",
"requesterNodeKey": "nodekey:caller",
"requesterEeVersion": "0.2.0",
"requesterEeProtocolVersion": "1.0",
"requesterWorkspaceIds": [],
"requesterCapabilities": []
}"#;
let req: HelloRequest = serde_json::from_str(json).expect("parses");
assert!(req.requester_advertised_tags.is_empty());
}
#[test]
fn caller_classification_maps_decline_codes_to_skip_reasons() {
for (code, expected) in [
(
HelloErrorCode::UnsupportedProtocolVersion,
"probe_malformed",
),
(
HelloErrorCode::DiscoveryConsentDenied,
"no_discovery_consent",
),
(HelloErrorCode::ResponderBusy, "probe_timeout"),
(HelloErrorCode::ResponderMeshDisabled, "non_ee"),
(HelloErrorCode::ResponderShieldsUp, "no_discovery_consent"),
(HelloErrorCode::ResponderUnauthenticatedTailscale, "non_ee"),
] {
assert_eq!(classify_decline_for_caller_skip_reason(code), expected);
}
}
#[test]
fn hello_error_code_round_trips_through_snake_case_serde() {
for code in [
HelloErrorCode::UnsupportedProtocolVersion,
HelloErrorCode::DiscoveryConsentDenied,
HelloErrorCode::ResponderBusy,
HelloErrorCode::ResponderMeshDisabled,
HelloErrorCode::ResponderShieldsUp,
HelloErrorCode::ResponderUnauthenticatedTailscale,
] {
let serialized = serde_json::to_string(&code).expect("serialize");
let deserialized: HelloErrorCode =
serde_json::from_str(&serialized).expect("deserialize");
assert_eq!(deserialized, code);
assert!(serialized.contains(code.as_str()));
}
}
}