use std::io::Read;
use std::net::SocketAddr;
use std::path::Path;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use zlayer_types::overlayd::{EdgeConfig, EDGE_CONFIG_VERSION};
use crate::edge::EdgeError;
const EDGE_TOKEN_PREFIX: &str = "zledge1.";
pub fn load_artifact(input: &str) -> Result<EdgeConfig, EdgeError> {
let content = read_source(input)?;
parse_content(&content)
}
fn read_source(input: &str) -> Result<String, EdgeError> {
if input == "-" {
let mut buf = String::new();
std::io::stdin().lock().read_to_string(&mut buf)?;
Ok(buf)
} else if Path::new(input).is_file() {
Ok(std::fs::read_to_string(input)?)
} else {
Ok(input.to_string())
}
}
fn parse_content(raw: &str) -> Result<EdgeConfig, EdgeError> {
let trimmed = raw.trim();
let cfg = if trimmed.starts_with(EDGE_TOKEN_PREFIX) {
EdgeConfig::from_token(trimmed)?
} else if trimmed.starts_with('{') {
serde_json::from_str(trimmed)
.map_err(|e| EdgeError::Artifact(format!("edge artifact JSON parse failed: {e}")))?
} else {
let bytes = URL_SAFE_NO_PAD
.decode(trimmed)
.map_err(|e| EdgeError::Artifact(format!("edge artifact base64 decode failed: {e}")))?;
serde_json::from_slice(&bytes)
.map_err(|e| EdgeError::Artifact(format!("edge artifact JSON parse failed: {e}")))?
};
validate(&cfg)?;
Ok(cfg)
}
fn validate(cfg: &EdgeConfig) -> Result<(), EdgeError> {
if cfg.version != EDGE_CONFIG_VERSION {
return Err(EdgeError::Artifact(format!(
"unsupported edge config version {} (expected {EDGE_CONFIG_VERSION})",
cfg.version
)));
}
if cfg.peers.len() != 1 {
return Err(EdgeError::Artifact(format!(
"edge config must carry exactly one peer (the minting node), found {}",
cfg.peers.len()
)));
}
let peer = &cfg.peers[0];
parse_endpoint_host_port(&peer.endpoint)?;
let nets = parse_allowed_ips(&peer.allowed_ips)?;
if nets.is_empty() {
return Err(EdgeError::Artifact(
"edge peer allowed_ips is empty (no routable CIDR)".to_string(),
));
}
Ok(())
}
fn parse_endpoint_host_port(endpoint: &str) -> Result<(), EdgeError> {
if let Ok(addr) = endpoint.parse::<SocketAddr>() {
if addr.port() == 0 {
return Err(EdgeError::Artifact(format!(
"edge peer endpoint '{endpoint}' has a zero port"
)));
}
if addr.ip().is_unspecified() {
return Err(EdgeError::Artifact(format!(
"edge peer endpoint '{endpoint}' uses the wildcard address"
)));
}
return Ok(());
}
let (host, port) = endpoint.rsplit_once(':').ok_or_else(|| {
EdgeError::Artifact(format!(
"edge peer endpoint '{endpoint}' is missing a ':port'"
))
})?;
if host.is_empty() {
return Err(EdgeError::Artifact(format!(
"edge peer endpoint '{endpoint}' has an empty host"
)));
}
let port: u16 = port.parse().map_err(|_| {
EdgeError::Artifact(format!(
"edge peer endpoint '{endpoint}' has a non-numeric port"
))
})?;
if port == 0 {
return Err(EdgeError::Artifact(format!(
"edge peer endpoint '{endpoint}' has a zero port"
)));
}
Ok(())
}
pub fn parse_allowed_ips(csv: &str) -> Result<Vec<ipnet::IpNet>, EdgeError> {
csv.split(',')
.map(str::trim)
.filter(|tok| !tok.is_empty())
.map(|tok| {
tok.parse::<ipnet::IpNet>()
.map_err(|e| EdgeError::Artifact(format!("invalid allowed_ips CIDR '{tok}': {e}")))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use zlayer_types::overlayd::PeerSpec;
fn sample_peer() -> PeerSpec {
PeerSpec {
public_key: "edge-pub-b64".into(),
endpoint: "203.0.113.7:51820".into(),
allowed_ips: "10.200.0.0/16,10.201.0.0/24".into(),
persistent_keepalive_secs: 25,
candidates: Vec::new(),
}
}
fn sample_config() -> EdgeConfig {
EdgeConfig {
version: EDGE_CONFIG_VERSION,
name: "ci-runner-7".into(),
overlay_ip: "10.200.0.9".parse().unwrap(),
prefix_len: 16,
private_key: "edge-priv-b64".into(),
public_key: "edge-pub-b64".into(),
peers: vec![sample_peer()],
dns_server: Some("10.200.0.1".parse().unwrap()),
dns_domain: Some("overlay".into()),
expires_at_unix: 1_700_000_600,
}
}
#[test]
fn token_path_round_trips() {
let cfg = sample_config();
let token = cfg.to_token();
assert!(token.starts_with(EDGE_TOKEN_PREFIX), "token was {token}");
assert_eq!(parse_content(&token).expect("token loads"), cfg);
}
#[test]
fn json_inline_path_round_trips() {
let cfg = sample_config();
let json = serde_json::to_string(&cfg).expect("serialize");
assert!(json.trim_start().starts_with('{'), "json was {json}");
assert_eq!(parse_content(&json).expect("json loads"), cfg);
}
#[test]
fn bare_base64_path_round_trips() {
let cfg = sample_config();
let json = serde_json::to_string(&cfg).expect("serialize");
let blob = URL_SAFE_NO_PAD.encode(json);
assert!(!blob.starts_with(EDGE_TOKEN_PREFIX));
assert!(!blob.starts_with('{'));
assert_eq!(parse_content(&blob).expect("base64 loads"), cfg);
}
#[test]
fn surrounding_whitespace_is_trimmed() {
let cfg = sample_config();
let token = format!(" \n{}\n ", cfg.to_token());
assert_eq!(parse_content(&token).expect("trims and loads"), cfg);
}
#[test]
fn hostname_and_ipv6_endpoints_accepted() {
let mut cfg = sample_config();
cfg.peers[0].endpoint = "node.example.com:51820".into();
assert!(parse_content(&cfg.to_token()).is_ok());
let mut cfg = sample_config();
cfg.peers[0].endpoint = "[2001:db8::1]:51820".into();
assert!(parse_content(&cfg.to_token()).is_ok());
}
#[test]
fn rejects_wrong_version_on_json_path() {
let mut cfg = sample_config();
cfg.version = EDGE_CONFIG_VERSION + 1;
let json = serde_json::to_string(&cfg).expect("serialize");
let err = parse_content(&json).expect_err("must reject");
assert!(matches!(err, EdgeError::Artifact(_)), "got {err:?}");
}
#[test]
fn rejects_two_peers() {
let mut cfg = sample_config();
cfg.peers.push(sample_peer());
let err = parse_content(&cfg.to_token()).expect_err("must reject");
assert!(matches!(err, EdgeError::Artifact(_)), "got {err:?}");
}
#[test]
fn rejects_zero_peers() {
let mut cfg = sample_config();
cfg.peers.clear();
let err = parse_content(&cfg.to_token()).expect_err("must reject");
assert!(matches!(err, EdgeError::Artifact(_)), "got {err:?}");
}
#[test]
fn rejects_portless_endpoint() {
let mut cfg = sample_config();
cfg.peers[0].endpoint = "1.2.3.4".into();
let err = parse_content(&cfg.to_token()).expect_err("must reject");
assert!(matches!(err, EdgeError::Artifact(_)), "got {err:?}");
}
#[test]
fn rejects_wildcard_zero_port_endpoint() {
let mut cfg = sample_config();
cfg.peers[0].endpoint = "0.0.0.0:0".into();
let err = parse_content(&cfg.to_token()).expect_err("must reject");
assert!(matches!(err, EdgeError::Artifact(_)), "got {err:?}");
}
#[test]
fn rejects_wildcard_address_even_with_valid_port() {
let mut cfg = sample_config();
cfg.peers[0].endpoint = "0.0.0.0:51820".into();
let err = parse_content(&cfg.to_token()).expect_err("must reject");
assert!(matches!(err, EdgeError::Artifact(_)), "got {err:?}");
}
#[test]
fn rejects_empty_allowed_ips() {
let mut cfg = sample_config();
cfg.peers[0].allowed_ips = String::new();
let err = parse_content(&cfg.to_token()).expect_err("must reject");
assert!(matches!(err, EdgeError::Artifact(_)), "got {err:?}");
}
#[test]
fn rejects_non_cidr_allowed_ips() {
let mut cfg = sample_config();
cfg.peers[0].allowed_ips = "not-a-cidr".into();
let err = parse_content(&cfg.to_token()).expect_err("must reject");
assert!(matches!(err, EdgeError::Artifact(_)), "got {err:?}");
}
#[test]
fn parse_allowed_ips_handles_multi_trailing_and_bad() {
let nets = parse_allowed_ips("10.200.0.0/16, 10.201.0.0/24 ,").expect("valid list");
assert_eq!(nets.len(), 2, "trailing comma + whitespace tolerated");
assert!(parse_allowed_ips("")
.expect("empty is Ok-but-empty")
.is_empty());
assert!(matches!(
parse_allowed_ips("10.0.0.0/8,garbage").expect_err("bad token"),
EdgeError::Artifact(_)
));
}
}