refactor(core): separate portable core from native runtime (#2451)

Create easytier-core as the portable owner of configuration,
connectivity, tunnels, peer and routing state, gateways, management,
the data plane, and instance lifecycle. Keep operating-system
integration, native protocol engines, process startup, and presentation
in easytier behind explicit Host capability adapters.

Create easytier-proto to own schemas, generated RPC types, descriptors,
and feature-scoped protocol slices. Remove runtime protobuf reflection
from core while preserving unknown route-peer fields across forwarding.

Normalize instance construction through CoreInstance, CoreHostAdapters,
CoreProcessRuntime, and InstanceManager. Make the runtime config store
the only authoritative mutable configuration after startup.

Move the portable TCP/UDP data plane into core and extract a generic
OperationBroker for completion, cancellation, disposal, and capacity
accounting. Expose the session-based FFI v2 completion API and keep the
WASI guest ABI, wire schemas, and adapters with core.

Migrate CLI, GUI, web, FFI, Android JNI, OHOS, uptime, and mobile
consumers to the shared manager and core state. Add explicit user/web
config ownership and revision-aware web reconciliation.

Preserve configuration, wire, and management behavior while fixing
regressions discovered by the full platform and integration matrix:

- inherit advertised relay capabilities in foreign networks;
- refresh OSPF peer state immediately after runtime config changes;
- restore CLI GlobalCtx event output without forcing GUI logging;
- retain legacy encryption names and standalone RPC tunnel metadata;
- restore ICMP host composition and fragmented UDP handling;
- use portable 64-bit atomics on 32-bit MIPS targets; and
- retain discarded operations until late cancellation completes.

Validate the refactor across 45 GitHub checks, including Linux, macOS,
Windows, FreeBSD, web, GUI, Android, OHOS, feature profiles, and
three-node and subnet-proxy integration tests.

BREAKING CHANGE: internal Rust module paths are not preserved. Legacy
native data-plane APIs are replaced by the session-based FFI v2 API.
The dedicated Android data-plane wrapper is removed.
This commit is contained in:
KKRainbow
2026-07-26 15:41:55 +08:00
committed by GitHub
parent 346f32d3d0
commit 021f523431
523 changed files with 102785 additions and 67067 deletions
+118
View File
@@ -0,0 +1,118 @@
use std::fmt::Display;
include!(concat!(env!("OUT_DIR"), "/acl.rs"));
include!(concat!(env!("OUT_DIR"), "/acl.serde.rs"));
impl Acl {
pub fn is_empty(&self) -> bool {
self.acl_v1.as_ref().map(|v1| v1.is_empty()).unwrap_or(true)
}
}
impl AclV1 {
pub fn is_empty(&self) -> bool {
let has_chains = !self.chains.is_empty();
let has_groups = self.group.as_ref().map(|g| !g.is_empty()).unwrap_or(false);
!has_chains && !has_groups
}
}
impl GroupInfo {
pub fn is_empty(&self) -> bool {
self.declares.is_empty() && self.members.is_empty()
}
}
#[cfg(feature = "api")]
impl Display for ConnTrackEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let src = self
.src_addr
.as_ref()
.map(|a| a.to_string())
.unwrap_or_else(|| "-".to_string());
let dst = self
.dst_addr
.as_ref()
.map(|a| a.to_string())
.unwrap_or_else(|| "-".to_string());
let last_seen = chrono::DateTime::<chrono::Utc>::from_timestamp(self.last_seen as i64, 0)
.unwrap()
.with_timezone(&chrono::Local);
let created_at = chrono::DateTime::<chrono::Utc>::from_timestamp(self.created_at as i64, 0)
.unwrap()
.with_timezone(&chrono::Local);
write!(
f,
"[src: {}, dst: {}, proto: {:?}, state: {:?}, pkts: {}, bytes: {}, created: {}, last_seen: {}]",
src,
dst,
Protocol::try_from(self.protocol).unwrap_or(Protocol::Unspecified),
ConnState::try_from(self.state).unwrap_or(ConnState::Invalid),
self.packet_count,
self.byte_count,
created_at,
last_seen
)
}
}
impl Display for Rule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[name: '{}', prio: {}, action: {:?}, enabled: {}, proto: {:?}, ports: {:?}, src_ports: {:?}, src_ips: {:?}, dst_ips: {:?}, stateful: {}, rate: {}, burst: {}]",
self.name,
self.priority,
Action::try_from(self.action).unwrap_or(Action::Noop),
self.enabled,
Protocol::try_from(self.protocol).unwrap_or(Protocol::Unspecified),
self.ports,
self.source_ports,
self.source_ips,
self.destination_ips,
self.stateful,
self.rate_limit,
self.burst_limit
)
}
}
impl Display for StatItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[pkts: {}, bytes: {}]",
self.packet_count, self.byte_count
)
}
}
#[cfg(feature = "api")]
impl Display for AclStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "AclStats:")?;
writeln!(f, " Global:")?;
for (k, v) in &self.global {
writeln!(f, " {}: {}", k, v)?;
}
writeln!(f, " ConnTrack:")?;
for entry in &self.conn_track {
writeln!(f, " {}", entry)?;
}
writeln!(f, " Rules:")?;
for rule_stat in &self.rules {
if let Some(rule) = &rule_stat.rule {
write!(f, " {} ", rule)?;
} else {
write!(f, " <default/none> ")?;
}
if let Some(stat) = &rule_stat.stat {
writeln!(f, "{}", stat)?;
} else {
writeln!(f)?;
}
}
Ok(())
}
}
+487
View File
@@ -0,0 +1,487 @@
pub mod config {
include!(concat!(env!("OUT_DIR"), "/api.config.rs"));
#[cfg(feature = "json-rpc")]
include!(concat!(env!("OUT_DIR"), "/api.config.serde.rs"));
pub struct Patchable<T> {
pub action: Option<ConfigPatchAction>,
pub value: Option<T>,
}
impl From<RoutePatch> for Patchable<cidr::Ipv4Cidr> {
fn from(value: RoutePatch) -> Self {
Patchable {
action: ConfigPatchAction::try_from(value.action).ok(),
value: value.cidr.map(Into::into),
}
}
}
impl From<ExitNodePatch> for Patchable<std::net::IpAddr> {
fn from(value: ExitNodePatch) -> Self {
Patchable {
action: ConfigPatchAction::try_from(value.action).ok(),
value: value.node.map(Into::into),
}
}
}
impl From<StringPatch> for Patchable<String> {
fn from(value: StringPatch) -> Self {
Patchable {
action: ConfigPatchAction::try_from(value.action).ok(),
value: Some(value.value),
}
}
}
impl From<UrlPatch> for Patchable<url::Url> {
fn from(value: UrlPatch) -> Self {
Patchable {
action: ConfigPatchAction::try_from(value.action).ok(),
value: value.url.map(Into::into),
}
}
}
pub fn patch_vec<T>(v: &mut Vec<T>, patches: Vec<Patchable<T>>)
where
T: PartialEq,
{
for patch in patches {
match patch.action {
Some(ConfigPatchAction::Add) => {
if let Some(value) = patch.value {
v.push(value);
}
}
Some(ConfigPatchAction::Remove) => {
if let Some(value) = patch.value {
v.retain(|x| x != &value);
}
}
Some(ConfigPatchAction::Clear) => {
v.clear();
}
None => {}
}
}
}
}
pub mod instance {
use std::fmt::{Display, Formatter};
include!(concat!(env!("OUT_DIR"), "/api.instance.rs"));
#[cfg(feature = "json-rpc")]
include!(concat!(env!("OUT_DIR"), "/api.instance.serde.rs"));
impl From<crate::core_peer::peer::PeerConnStats> for PeerConnStats {
fn from(value: crate::core_peer::peer::PeerConnStats) -> Self {
Self {
rx_bytes: value.rx_bytes,
tx_bytes: value.tx_bytes,
rx_packets: value.rx_packets,
tx_packets: value.tx_packets,
latency_us: value.latency_us,
}
}
}
impl From<crate::core_peer::peer::PeerConnInfo> for PeerConnInfo {
fn from(value: crate::core_peer::peer::PeerConnInfo) -> Self {
Self {
conn_id: value.conn_id,
my_peer_id: value.my_peer_id,
peer_id: value.peer_id,
features: value.features,
tunnel: value.tunnel,
stats: value.stats.map(Into::into),
loss_rate: value.loss_rate,
is_client: value.is_client,
network_name: value.network_name,
is_closed: value.is_closed,
noise_local_static_pubkey: value.noise_local_static_pubkey,
noise_remote_static_pubkey: value.noise_remote_static_pubkey,
secure_auth_level: value.secure_auth_level,
peer_identity_type: value.peer_identity_type,
}
}
}
impl From<crate::core_peer::peer::PeerInfo> for PeerInfo {
fn from(value: crate::core_peer::peer::PeerInfo) -> Self {
Self {
peer_id: value.peer_id,
conns: value.conns.into_iter().map(Into::into).collect(),
default_conn_id: value.default_conn_id,
directly_connected_conns: value.directly_connected_conns,
}
}
}
impl From<crate::core_peer::peer::Route> for Route {
fn from(value: crate::core_peer::peer::Route) -> Self {
Self {
peer_id: value.peer_id,
ipv4_addr: value.ipv4_addr,
next_hop_peer_id: value.next_hop_peer_id,
cost: value.cost,
path_latency: value.path_latency,
proxy_cidrs: value.proxy_cidrs,
hostname: value.hostname,
stun_info: value.stun_info,
inst_id: value.inst_id,
version: value.version,
feature_flag: value.feature_flag,
next_hop_peer_id_latency_first: value.next_hop_peer_id_latency_first,
cost_latency_first: value.cost_latency_first,
path_latency_latency_first: value.path_latency_latency_first,
ipv6_addr: value.ipv6_addr,
public_ipv6_addr: value.public_ipv6_addr,
ipv6_public_addr_prefix: value.ipv6_public_addr_prefix,
}
}
}
impl From<crate::core_peer::peer::PublicIpv6LeaseInfo> for PublicIpv6LeaseInfo {
fn from(value: crate::core_peer::peer::PublicIpv6LeaseInfo) -> Self {
Self {
peer_id: value.peer_id,
inst_id: value.inst_id,
leased_addr: value.leased_addr,
valid_until_unix_seconds: value.valid_until_unix_seconds,
reused: value.reused,
}
}
}
impl From<crate::core_peer::peer::ListPublicIpv6InfoResponse> for ListPublicIpv6InfoResponse {
fn from(value: crate::core_peer::peer::ListPublicIpv6InfoResponse) -> Self {
Self {
provider_prefix: value.provider_prefix,
provider_leases: value.provider_leases.into_iter().map(Into::into).collect(),
}
}
}
impl Display for PeerConnInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PeerConnInfo")
.field("my_peer_id", &self.my_peer_id)
.field("dst_peer_id", &self.peer_id)
.field("tunnel_info", &self.tunnel)
.finish()
}
}
impl PeerRoutePair {
pub fn get_latency_ms(&self) -> Option<f64> {
let mut ret = u64::MAX;
let p = self.peer.as_ref()?;
let default_conn_id = p.default_conn_id.map(|id| id.to_string());
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
if default_conn_id == Some(conn.conn_id.to_string()) {
return Some(f64::from(stats.latency_us as u32) / 1000.0);
}
ret = ret.min(stats.latency_us);
}
if ret == u64::MAX {
None
} else {
Some(f64::from(ret as u32) / 1000.0)
}
}
pub fn get_rx_bytes(&self) -> Option<u64> {
let mut ret = 0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret += stats.rx_bytes;
}
if ret == 0 { None } else { Some(ret) }
}
pub fn get_tx_bytes(&self) -> Option<u64> {
let mut ret = 0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret += stats.tx_bytes;
}
if ret == 0 { None } else { Some(ret) }
}
pub fn get_loss_rate(&self) -> Option<f64> {
let p = self.peer.as_ref()?;
let default_conn_id = p.default_conn_id.map(|id| id.to_string());
let mut ret = None;
for conn in p.conns.iter() {
if default_conn_id == Some(conn.conn_id.to_string()) {
return Some(conn.loss_rate as f64);
}
ret.get_or_insert(conn.loss_rate as f64);
}
ret
}
fn get_tunnel_proto_str(tunnel_info: &super::super::common::TunnelInfo) -> String {
tunnel_info.display_tunnel_type()
}
pub fn get_conn_protos(&self) -> Option<Vec<String>> {
let mut ret = vec![];
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(tunnel_info) = &conn.tunnel else {
continue;
};
// insert if not exists
let tunnel_type = Self::get_tunnel_proto_str(tunnel_info);
if !ret.contains(&tunnel_type) {
ret.push(tunnel_type);
}
}
if ret.is_empty() { None } else { Some(ret) }
}
pub fn get_udp_nat_type(&self) -> String {
use crate::proto::common::NatType;
let mut ret = NatType::Unknown;
if let Some(r) = &self.route.clone().unwrap_or_default().stun_info {
ret = NatType::try_from(r.udp_nat_type).unwrap();
}
format!("{:?}", ret)
}
}
pub fn list_peer_route_pair(peers: Vec<PeerInfo>, routes: Vec<Route>) -> Vec<PeerRoutePair> {
let mut pairs: Vec<PeerRoutePair> = vec![];
for route in routes.iter() {
let peer = peers.iter().find(|peer| peer.peer_id == route.peer_id);
let pair = PeerRoutePair {
route: Some(route.clone()),
peer: peer.cloned(),
};
pairs.push(pair);
}
pairs.sort_by(|a, b| {
let a_is_public_server = a
.route
.as_ref()
.and_then(|r| r.feature_flag.as_ref())
.is_some_and(|f| f.is_public_server);
let b_is_public_server = b
.route
.as_ref()
.and_then(|r| r.feature_flag.as_ref())
.is_some_and(|f| f.is_public_server);
if a_is_public_server != b_is_public_server {
return if a_is_public_server {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
};
}
let a_ip = a
.route
.as_ref()
.and_then(|r| r.ipv4_addr.as_ref())
.and_then(|ipv4| ipv4.address.as_ref())
.map_or(0, |addr| addr.addr);
let b_ip = b
.route
.as_ref()
.and_then(|r| r.ipv4_addr.as_ref())
.and_then(|ipv4| ipv4.address.as_ref())
.map_or(0, |addr| addr.addr);
a_ip.cmp(&b_ip)
});
pairs
}
}
pub mod logger {
include!(concat!(env!("OUT_DIR"), "/api.logger.rs"));
#[cfg(feature = "json-rpc")]
include!(concat!(env!("OUT_DIR"), "/api.logger.serde.rs"));
}
pub mod manage {
include!(concat!(env!("OUT_DIR"), "/api.manage.rs"));
#[cfg(feature = "json-rpc")]
include!(concat!(env!("OUT_DIR"), "/api.manage.serde.rs"));
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use prost::Message;
use super::instance::{PeerConnInfo, PeerInfo, PeerRoutePair};
use super::manage::{
ListNetworkInstanceRequest, ListNetworkInstanceResponse, WebClientService,
WebClientServiceClient, WebClientServiceDescriptor, WebClientServiceMethodDescriptor,
};
use crate::proto::common::Uuid;
use crate::proto::rpc_types::controller::BaseController;
use crate::proto::rpc_types::descriptor::ServiceDescriptor;
use crate::proto::rpc_types::error::Error;
use crate::proto::rpc_types::handler::Handler;
#[derive(Clone, Default)]
struct WebClientServiceJsonCallHandler;
#[async_trait::async_trait]
impl Handler for WebClientServiceJsonCallHandler {
type Descriptor = WebClientServiceDescriptor;
type Controller = BaseController;
async fn call(
&self,
_ctrl: Self::Controller,
method: <Self::Descriptor as ServiceDescriptor>::Method,
input: Bytes,
) -> crate::proto::rpc_types::error::Result<Bytes> {
match method {
WebClientServiceMethodDescriptor::ListNetworkInstance => {
let _req = ListNetworkInstanceRequest::decode(input.as_ref()).unwrap();
let resp = ListNetworkInstanceResponse {
inst_ids: vec![Uuid {
part1: 1,
part2: 2,
part3: 3,
part4: 4,
}],
};
Ok(Bytes::from(resp.encode_to_vec()))
}
_ => Err(Error::ExecutionError(anyhow::anyhow!(
"unsupported method in test handler"
))),
}
}
}
#[tokio::test]
async fn web_client_service_call_json_method_supports_snake_and_proto_method_name() {
let client = WebClientServiceClient::new(WebClientServiceJsonCallHandler);
let snake_result = client
.json_call_method(
BaseController::default(),
"list_network_instance",
serde_json::json!({}),
)
.await
.unwrap();
assert_eq!(
snake_result["inst_ids"][0],
serde_json::json!({
"part1": 1,
"part2": 2,
"part3": 3,
"part4": 4
})
);
let proto_result = client
.json_call_method(
BaseController::default(),
"ListNetworkInstance",
serde_json::json!({}),
)
.await
.unwrap();
assert_eq!(proto_result["inst_ids"].as_array().unwrap().len(), 1);
}
#[tokio::test]
async fn web_client_service_call_json_method_rejects_unknown_method() {
let client = WebClientServiceClient::new(WebClientServiceJsonCallHandler);
let ret = client
.json_call_method(
BaseController::default(),
"not_exist_method",
serde_json::json!({}),
)
.await;
assert!(ret.is_err());
}
#[test]
fn peer_route_pair_loss_rate_uses_default_conn() {
let default_conn_id = uuid::Uuid::new_v4();
let pair = PeerRoutePair {
peer: Some(PeerInfo {
default_conn_id: Some(default_conn_id.into()),
conns: vec![
PeerConnInfo {
conn_id: uuid::Uuid::new_v4().to_string(),
loss_rate: 0.8,
..Default::default()
},
PeerConnInfo {
conn_id: default_conn_id.to_string(),
loss_rate: 0.4,
..Default::default()
},
],
..Default::default()
}),
..Default::default()
};
assert!(
pair.get_loss_rate()
.is_some_and(|loss_rate| (loss_rate - 0.4).abs() < 1e-6)
);
}
#[test]
fn peer_route_pair_loss_rate_falls_back_to_first_conn() {
let pair = PeerRoutePair {
peer: Some(PeerInfo {
conns: vec![
PeerConnInfo {
conn_id: uuid::Uuid::new_v4().to_string(),
loss_rate: 0.0,
..Default::default()
},
PeerConnInfo {
conn_id: uuid::Uuid::new_v4().to_string(),
loss_rate: 0.7,
..Default::default()
},
],
..Default::default()
}),
..Default::default()
};
assert_eq!(pair.get_loss_rate(), Some(0.0));
}
}
+659
View File
@@ -0,0 +1,659 @@
use anyhow::Context;
use base64::{Engine as _, prelude::BASE64_STANDARD};
use std::time::SystemTime;
use std::{
fmt::{self, Display},
str::FromStr,
};
const IP_SCHEMES: &[&str] = &["tcp", "udp", "wg", "quic", "ws", "wss", "faketcp"];
include!(concat!(env!("OUT_DIR"), "/common.rs"));
include!(concat!(env!("OUT_DIR"), "/common.serde.rs"));
pub trait TimestampExt {
fn now() -> Self;
}
#[cfg(feature = "json-rpc")]
pub type RuntimeTimestamp = prost_wkt_types::Timestamp;
#[cfg(not(feature = "json-rpc"))]
pub type RuntimeTimestamp = prost_types::Timestamp;
impl TimestampExt for RuntimeTimestamp {
fn now() -> Self {
SystemTime::now().into()
}
}
impl From<uuid::Uuid> for Uuid {
fn from(uuid: uuid::Uuid) -> Self {
let (high, low) = uuid.as_u64_pair();
Uuid {
part1: (high >> 32) as u32,
part2: (high & 0xFFFFFFFF) as u32,
part3: (low >> 32) as u32,
part4: (low & 0xFFFFFFFF) as u32,
}
}
}
impl From<Uuid> for uuid::Uuid {
fn from(uuid: Uuid) -> Self {
uuid::Uuid::from_u64_pair(
(u64::from(uuid.part1) << 32) | u64::from(uuid.part2),
(u64::from(uuid.part3) << 32) | u64::from(uuid.part4),
)
}
}
impl From<String> for Uuid {
fn from(value: String) -> Self {
uuid::Uuid::parse_str(&value).unwrap().into()
}
}
impl fmt::Display for Uuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", uuid::Uuid::from(*self))
}
}
impl fmt::Debug for Uuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", uuid::Uuid::from(*self))
}
}
impl From<std::net::Ipv4Addr> for Ipv4Addr {
fn from(value: std::net::Ipv4Addr) -> Self {
Self {
addr: u32::from_be_bytes(value.octets()),
}
}
}
impl From<Ipv4Addr> for std::net::Ipv4Addr {
fn from(value: Ipv4Addr) -> Self {
std::net::Ipv4Addr::from(value.addr)
}
}
impl Display for Ipv4Addr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", std::net::Ipv4Addr::from(self.addr))
}
}
impl From<std::net::Ipv6Addr> for Ipv6Addr {
fn from(value: std::net::Ipv6Addr) -> Self {
let b = value.octets();
Self {
part1: u32::from_be_bytes([b[0], b[1], b[2], b[3]]),
part2: u32::from_be_bytes([b[4], b[5], b[6], b[7]]),
part3: u32::from_be_bytes([b[8], b[9], b[10], b[11]]),
part4: u32::from_be_bytes([b[12], b[13], b[14], b[15]]),
}
}
}
impl From<Ipv6Addr> for std::net::Ipv6Addr {
fn from(value: Ipv6Addr) -> Self {
let part1 = value.part1.to_be_bytes();
let part2 = value.part2.to_be_bytes();
let part3 = value.part3.to_be_bytes();
let part4 = value.part4.to_be_bytes();
std::net::Ipv6Addr::from([
part1[0], part1[1], part1[2], part1[3], part2[0], part2[1], part2[2], part2[3],
part3[0], part3[1], part3[2], part3[3], part4[0], part4[1], part4[2], part4[3],
])
}
}
impl Display for Ipv6Addr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", std::net::Ipv6Addr::from(*self))
}
}
impl From<cidr::Ipv4Inet> for Ipv4Inet {
fn from(value: cidr::Ipv4Inet) -> Self {
Ipv4Inet {
address: Some(value.address().into()),
network_length: value.network_length() as u32,
}
}
}
impl From<std::net::IpAddr> for IpAddr {
fn from(value: std::net::IpAddr) -> Self {
match value {
std::net::IpAddr::V4(v4) => IpAddr {
ip: Some(ip_addr::Ip::Ipv4(Ipv4Addr::from(v4))),
},
std::net::IpAddr::V6(v6) => IpAddr {
ip: Some(ip_addr::Ip::Ipv6(Ipv6Addr::from(v6))),
},
}
}
}
impl From<IpAddr> for std::net::IpAddr {
fn from(value: IpAddr) -> Self {
match value.ip {
Some(ip_addr::Ip::Ipv4(v4)) => std::net::IpAddr::V4(v4.into()),
Some(ip_addr::Ip::Ipv6(v6)) => std::net::IpAddr::V6(v6.into()),
None => panic!("IpAddr is None"),
}
}
}
impl Display for IpAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", std::net::IpAddr::from(*self))
}
}
impl FromStr for IpAddr {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(IpAddr::from(std::net::IpAddr::from_str(s)?))
}
}
impl From<Ipv4Inet> for cidr::Ipv4Inet {
fn from(value: Ipv4Inet) -> Self {
cidr::Ipv4Inet::new(
value.address.unwrap_or_default().into(),
value.network_length as u8,
)
.unwrap()
}
}
impl From<Ipv4Inet> for cidr::Ipv4Cidr {
fn from(value: Ipv4Inet) -> Self {
cidr::Ipv4Cidr::new(
value.address.unwrap_or_default().into(),
value.network_length as u8,
)
.unwrap()
}
}
impl fmt::Display for Ipv4Inet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", cidr::Ipv4Inet::from(*self))
}
}
impl FromStr for Ipv4Inet {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Ipv4Inet::from(
cidr::Ipv4Inet::from_str(s).with_context(|| "Failed to parse Ipv4Inet")?,
))
}
}
impl From<cidr::Ipv6Inet> for Ipv6Inet {
fn from(value: cidr::Ipv6Inet) -> Self {
Ipv6Inet {
address: Some(value.address().into()),
network_length: value.network_length() as u32,
}
}
}
impl From<Ipv6Inet> for cidr::Ipv6Inet {
fn from(value: Ipv6Inet) -> Self {
cidr::Ipv6Inet::new(
value.address.unwrap_or_default().into(),
value.network_length as u8,
)
.unwrap()
}
}
impl fmt::Display for Ipv6Inet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", cidr::Ipv6Inet::from(*self))
}
}
impl FromStr for Ipv6Inet {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Ipv6Inet::from(
cidr::Ipv6Inet::from_str(s).with_context(|| "Failed to parse Ipv6Inet")?,
))
}
}
impl From<cidr::IpInet> for IpInet {
fn from(value: cidr::IpInet) -> Self {
match value {
cidr::IpInet::V4(v4) => IpInet {
ip: Some(ip_inet::Ip::Ipv4(Ipv4Inet::from(v4))),
},
cidr::IpInet::V6(v6) => IpInet {
ip: Some(ip_inet::Ip::Ipv6(Ipv6Inet::from(v6))),
},
}
}
}
impl From<IpInet> for cidr::IpInet {
fn from(value: IpInet) -> Self {
match value.ip {
Some(ip_inet::Ip::Ipv4(v4)) => cidr::IpInet::V4(v4.into()),
Some(ip_inet::Ip::Ipv6(v6)) => cidr::IpInet::V6(v6.into()),
None => panic!("IpInet is None"),
}
}
}
impl Display for IpInet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", cidr::IpInet::from(*self))
}
}
impl FromStr for IpInet {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(IpInet::from(cidr::IpInet::from_str(s)?))
}
}
impl From<url::Url> for Url {
fn from(value: url::Url) -> Self {
Url { url: value.into() }
}
}
impl TryFrom<&Url> for url::Url {
type Error = url::ParseError;
fn try_from(value: &Url) -> Result<Self, Self::Error> {
value.url.parse()
}
}
impl From<Url> for url::Url {
fn from(value: Url) -> Self {
(&value).try_into().expect("failed to parse url")
}
}
impl FromStr for Url {
type Err = url::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
url::Url::try_from(s).map(Into::into)
}
}
impl fmt::Display for Url {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.url)
}
}
fn split_tunnel_scheme(raw_scheme: &str) -> Option<(&str, &'static str, bool)> {
for &scheme in IP_SCHEMES {
if let Some(base) = raw_scheme.strip_suffix('6')
&& let Some(prefix) = base.strip_suffix(scheme)
&& (prefix.is_empty() || prefix.ends_with('-'))
{
return Some((prefix, scheme, true));
}
if let Some(prefix) = raw_scheme.strip_suffix(scheme)
&& (prefix.is_empty() || prefix.ends_with('-'))
{
return Some((prefix, scheme, false));
}
}
None
}
fn normalize_tunnel_scheme(raw_scheme: &str, is_ipv6: bool) -> Option<String> {
let (prefix, scheme, had_ipv6_suffix) = split_tunnel_scheme(raw_scheme)?;
let suffix = if is_ipv6 || had_ipv6_suffix { "6" } else { "" };
Some(format!("{prefix}{scheme}{suffix}"))
}
fn infer_tunnel_ipv6(raw: &str) -> Option<bool> {
let (_, rest) = raw.split_once("://")?;
if rest.starts_with('[') {
return Some(true);
}
match url::Url::parse(raw).ok()?.host() {
Some(url::Host::Ipv4(_)) => Some(false),
Some(url::Host::Ipv6(_)) => Some(true),
Some(url::Host::Domain(_)) | None => None,
}
}
fn normalize_tunnel_port(raw_port: &str, is_ipv6: bool) -> Option<u16> {
if let Ok(port) = raw_port.parse::<u16>() {
return Some(port);
}
if is_ipv6 && raw_port.ends_with('6') {
return raw_port[..raw_port.len() - 1].parse::<u16>().ok();
}
None
}
fn normalize_tunnel_url(raw: &str, fallback_ipv6: Option<bool>) -> Option<String> {
let (raw_scheme, rest) = raw.split_once("://")?;
if let Some(rest) = rest.strip_prefix('[') {
let (host, remainder) = rest.split_once(']')?;
let scheme = normalize_tunnel_scheme(raw_scheme, true)?;
if remainder.is_empty() {
return Some(format!("{scheme}://[{host}]"));
}
let raw_port = remainder.strip_prefix(':')?;
let port = normalize_tunnel_port(raw_port, true)?;
return Some(format!("{scheme}://[{host}]:{port}"));
}
let is_ipv6 = infer_tunnel_ipv6(raw).or(fallback_ipv6).unwrap_or(false);
let scheme = normalize_tunnel_scheme(raw_scheme, is_ipv6)?;
if let Ok(url) = url::Url::parse(raw) {
let host = match url.host()? {
url::Host::Ipv4(host) => host.to_string(),
url::Host::Ipv6(host) => format!("[{host}]"),
url::Host::Domain(host) => host.to_string(),
};
return Some(match url.port_or_known_default() {
Some(port) => format!("{scheme}://{host}:{port}"),
None => format!("{scheme}://{host}"),
});
}
let (host, raw_port) = rest.rsplit_once(':')?;
let port = normalize_tunnel_port(raw_port, is_ipv6)?;
Some(format!("{scheme}://{host}:{port}"))
}
impl Url {
pub fn is_ipv6_tunnel_endpoint(&self) -> bool {
infer_tunnel_ipv6(&self.url).unwrap_or(false)
}
pub fn normalized_tunnel_display(&self) -> String {
normalize_tunnel_url(&self.url, None).unwrap_or_else(|| self.url.clone())
}
}
impl From<std::net::SocketAddr> for SocketAddr {
fn from(value: std::net::SocketAddr) -> Self {
match value {
std::net::SocketAddr::V4(v4) => SocketAddr {
ip: Some(socket_addr::Ip::Ipv4((*v4.ip()).into())),
port: v4.port() as u32,
},
std::net::SocketAddr::V6(v6) => SocketAddr {
ip: Some(socket_addr::Ip::Ipv6((*v6.ip()).into())),
port: v6.port() as u32,
},
}
}
}
impl From<SocketAddr> for std::net::SocketAddr {
fn from(value: SocketAddr) -> Self {
if value.ip.is_none() {
return "0.0.0.0:0".parse().unwrap();
}
match value.ip.unwrap() {
socket_addr::Ip::Ipv4(ip) => std::net::SocketAddr::V4(std::net::SocketAddrV4::new(
std::net::Ipv4Addr::from(ip),
value.port as u16,
)),
socket_addr::Ip::Ipv6(ip) => std::net::SocketAddr::V6(std::net::SocketAddrV6::new(
std::net::Ipv6Addr::from(ip),
value.port as u16,
0,
0,
)),
}
}
}
impl Display for SocketAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", std::net::SocketAddr::from(*self))
}
}
impl TunnelInfo {
pub fn effective_remote_addr(&self) -> Option<&Url> {
self.resolved_remote_addr
.as_ref()
.or(self.remote_addr.as_ref())
}
pub fn display_tunnel_type(&self) -> String {
let is_ipv6 = infer_tunnel_ipv6(&self.tunnel_type).or_else(|| {
self.resolved_remote_addr
.as_ref()
.or(self.local_addr.as_ref())
.or(self.remote_addr.as_ref())
.map(Url::is_ipv6_tunnel_endpoint)
});
if self.tunnel_type.contains("://") {
normalize_tunnel_url(&self.tunnel_type, is_ipv6)
.unwrap_or_else(|| self.tunnel_type.clone())
} else {
is_ipv6
.and_then(|is_ipv6| normalize_tunnel_scheme(&self.tunnel_type, is_ipv6))
.unwrap_or_else(|| self.tunnel_type.clone())
}
}
pub fn display_remote_addr(&self) -> Option<String> {
self.effective_remote_addr()
.map(Url::normalized_tunnel_display)
}
}
impl fmt::Debug for Ipv4Addr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let std_ipv4_addr = std::net::Ipv4Addr::from(*self);
write!(f, "{}", std_ipv4_addr)
}
}
impl fmt::Debug for Ipv6Addr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let std_ipv6_addr = std::net::Ipv6Addr::from(*self);
write!(f, "{}", std_ipv6_addr)
}
}
impl SecureModeConfig {
pub fn private_key(&self) -> anyhow::Result<x25519_dalek::StaticSecret> {
let local_private_key = self
.local_private_key
.as_ref()
.ok_or_else(|| anyhow::anyhow!("local private key is not set"))?;
let k = BASE64_STANDARD
.decode(local_private_key)
.with_context(|| format!("failed to decode private key: {}", local_private_key))?;
// convert vec to 32b array
let len = k.len();
let k: [u8; 32] = k
.try_into()
.map_err(|_| anyhow::anyhow!("invalid private key length: {}", len))?;
Ok(x25519_dalek::StaticSecret::from(k))
}
pub fn public_key(&self) -> anyhow::Result<x25519_dalek::PublicKey> {
let local_public_key = self
.local_public_key
.as_ref()
.ok_or_else(|| anyhow::anyhow!("local public key is not set"))?;
let k = BASE64_STANDARD
.decode(local_public_key)
.with_context(|| format!("failed to decode public key: {}", local_public_key))?;
// convert vec to 32b array
let len = k.len();
let k: [u8; 32] = k
.try_into()
.map_err(|_| anyhow::anyhow!("invalid public key length: {}", len))?;
Ok(x25519_dalek::PublicKey::from(k))
}
}
#[cfg(test)]
mod tests {
use super::{TunnelInfo, Url, normalize_tunnel_url};
fn assert_ipv6_tunnel_normalization(scheme: &str, port: u16) {
let expected = format!("{scheme}6://[2001:db8::1]:{port}");
assert_eq!(
normalize_tunnel_url(&format!("{scheme}://[2001:db8::1]:{port}"), None).as_deref(),
Some(expected.as_str())
);
}
#[test]
fn normalize_plain_ipv6_tunnel_url() {
let url = Url {
url: "tcp://[2001:db8::1]:11010".to_string(),
};
assert_eq!(
url.normalized_tunnel_display(),
"tcp6://[2001:db8::1]:11010"
);
assert!(url.is_ipv6_tunnel_endpoint());
}
#[test]
fn normalize_all_ipv6_tunnel_urls() {
assert_ipv6_tunnel_normalization("tcp", 11010);
assert_ipv6_tunnel_normalization("udp", 11010);
assert_ipv6_tunnel_normalization("wg", 11011);
assert_ipv6_tunnel_normalization("quic", 11012);
assert_ipv6_tunnel_normalization("ws", 80);
assert_ipv6_tunnel_normalization("wss", 443);
assert_ipv6_tunnel_normalization("faketcp", 11013);
}
#[test]
fn normalize_composite_ipv6_tunnel_url() {
assert_eq!(
normalize_tunnel_url("txt-tcp://[2001:db8::1]:11010", None).as_deref(),
Some("txt-tcp6://[2001:db8::1]:11010")
);
}
#[test]
fn recover_malformed_composite_ipv6_tunnel_url() {
assert_eq!(
normalize_tunnel_url("txt-tcp://[2001:db8::1]:110106", None).as_deref(),
Some("txt-tcp6://[2001:db8::1]:11010")
);
}
#[test]
fn keep_normalized_ipv6_tunnel_url_stable() {
assert_eq!(
normalize_tunnel_url("tcp6://[2001:db8::1]:11010", None).as_deref(),
Some("tcp6://[2001:db8::1]:11010")
);
}
#[test]
fn normalize_ipv6_tunnel_url_without_explicit_port() {
assert_eq!(
normalize_tunnel_url("tcp://[2001:db8::1]", None).as_deref(),
Some("tcp6://[2001:db8::1]")
);
}
#[test]
fn keep_domain_host_unbracketed_when_ipv6_falls_back() {
assert_eq!(
normalize_tunnel_url("tcp://localhost:11010", Some(true)).as_deref(),
Some("tcp6://localhost:11010")
);
}
#[test]
fn tunnel_info_display_tunnel_type_preserves_composite_prefix() {
let tunnel = TunnelInfo {
tunnel_type: "txt-tcp://[2001:db8::2]:110106".to_string(),
local_addr: None,
remote_addr: Some(Url {
url: "txt://et.example.com".to_string(),
}),
resolved_remote_addr: None,
};
assert_eq!(
tunnel.display_tunnel_type(),
"txt-tcp6://[2001:db8::2]:11010"
);
}
#[test]
fn tunnel_info_display_tunnel_type_uses_remote_addr_fallback() {
let tunnel = TunnelInfo {
tunnel_type: "tcp".to_string(),
local_addr: None,
remote_addr: Some(Url {
url: "tcp://[2001:db8::2]:11010".to_string(),
}),
resolved_remote_addr: None,
};
assert_eq!(tunnel.display_tunnel_type(), "tcp6");
assert_eq!(
tunnel.display_remote_addr().as_deref(),
Some("tcp6://[2001:db8::2]:11010")
);
}
#[test]
fn tunnel_info_prefers_resolved_remote_addr() {
let tunnel = TunnelInfo {
tunnel_type: "txt-tcp".to_string(),
local_addr: None,
remote_addr: Some(Url {
url: "txt://et.example.com".to_string(),
}),
resolved_remote_addr: Some(Url {
url: "tcp://[2001:db8::3]:11010".to_string(),
}),
};
assert_eq!(tunnel.display_tunnel_type(), "txt-tcp6");
assert_eq!(
tunnel.display_remote_addr().as_deref(),
Some("tcp6://[2001:db8::3]:11010")
);
assert_eq!(
tunnel.effective_remote_addr().map(|url| url.url.as_str()),
Some("tcp://[2001:db8::3]:11010")
);
}
}
+3
View File
@@ -0,0 +1,3 @@
include!(concat!(env!("OUT_DIR"), "/core_config.rs"));
#[cfg(feature = "json-rpc")]
include!(concat!(env!("OUT_DIR"), "/core_config.serde.rs"));
+5
View File
@@ -0,0 +1,5 @@
pub mod peer {
include!(concat!(env!("OUT_DIR"), "/core.peer.rs"));
#[cfg(feature = "json-rpc")]
include!(concat!(env!("OUT_DIR"), "/core.peer.serde.rs"));
}
+81
View File
@@ -0,0 +1,81 @@
#![allow(clippy::module_inception)]
use super::rpc_types;
include!(concat!(env!("OUT_DIR"), "/error.rs"));
include!(concat!(env!("OUT_DIR"), "/error.serde.rs"));
impl From<&rpc_types::error::Error> for Error {
fn from(e: &rpc_types::error::Error) -> Self {
use super::error::error::ErrorKind as ProtoError;
match e {
rpc_types::error::Error::ExecutionError(e) => Self {
error_kind: Some(ProtoError::ExecuteError(ExecuteError {
error_message: format!("{:?}", e),
})),
},
rpc_types::error::Error::DecodeError => Self {
error_kind: Some(ProtoError::ProstDecodeError(ProstDecodeError {})),
},
rpc_types::error::Error::EncodeError => Self {
error_kind: Some(ProtoError::ProstEncodeError(ProstEncodeError {})),
},
rpc_types::error::Error::InvalidMethodIndex(m, s) => Self {
error_kind: Some(ProtoError::InvalidMethodIndex(InvalidMethodIndex {
method_index: *m as u32,
service_name: format!("{:?}", s),
})),
},
rpc_types::error::Error::InvalidServiceKey(s, _) => Self {
error_kind: Some(ProtoError::InvalidService(InvalidService {
service_name: format!("{:?}", s),
})),
},
rpc_types::error::Error::MalformatRpcPacket(e) => Self {
error_kind: Some(ProtoError::MalformatRpcPacket(MalformatRpcPacket {
error_message: format!("{:?}", e),
})),
},
rpc_types::error::Error::Timeout(e) => Self {
error_kind: Some(ProtoError::Timeout(Timeout {
error_message: format!("{:?}", e),
})),
},
#[allow(unreachable_patterns)]
e => Self {
error_kind: Some(ProtoError::OtherError(OtherError {
error_message: format!("{:?}", e),
})),
},
}
}
}
impl From<&Error> for rpc_types::error::Error {
fn from(e: &Error) -> Self {
use super::error::error::ErrorKind as ProtoError;
match &e.error_kind {
Some(ProtoError::ExecuteError(e)) => {
Self::ExecutionError(anyhow::anyhow!(e.error_message.clone()))
}
Some(ProtoError::ProstDecodeError(_)) => Self::DecodeError,
Some(ProtoError::ProstEncodeError(_)) => Self::EncodeError,
Some(ProtoError::InvalidMethodIndex(e)) => {
Self::InvalidMethodIndex(e.method_index as u8, e.service_name.clone())
}
Some(ProtoError::InvalidService(e)) => {
Self::InvalidServiceKey(e.service_name.clone(), "".to_string())
}
Some(ProtoError::MalformatRpcPacket(e)) => {
Self::MalformatRpcPacket(e.error_message.clone())
}
Some(ProtoError::Timeout(e)) => {
Self::ExecutionError(anyhow::anyhow!(e.error_message.clone()))
}
Some(ProtoError::OtherError(e)) => {
Self::ExecutionError(anyhow::anyhow!(e.error_message.clone()))
}
None => Self::ExecutionError(anyhow::anyhow!("unknown error {:?}", e)),
}
}
}
+33
View File
@@ -0,0 +1,33 @@
#[cfg(feature = "core")]
pub mod rpc_types;
#[cfg(feature = "core")]
pub mod acl;
#[cfg(feature = "api")]
pub mod api;
#[cfg(feature = "core")]
pub mod common;
#[cfg(feature = "core")]
pub mod core_config;
#[cfg(feature = "core")]
pub mod core_peer;
#[cfg(feature = "core")]
pub mod error;
#[cfg(all(feature = "api", feature = "magic-dns"))]
pub mod magic_dns;
#[cfg(feature = "core")]
pub mod peer_rpc;
#[cfg(feature = "api")]
pub mod tests;
#[cfg(feature = "api")]
pub mod web;
pub const DESCRIPTOR_POOL_BYTES: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin"));
pub const ALL_DESCRIPTOR_BYTES: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/descriptors.bin"));
pub mod proto {
pub use crate::*;
}
+3
View File
@@ -0,0 +1,3 @@
include!(concat!(env!("OUT_DIR"), "/magic_dns.rs"));
#[cfg(feature = "json-rpc")]
include!(concat!(env!("OUT_DIR"), "/magic_dns.serde.rs"));
+512
View File
@@ -0,0 +1,512 @@
use hmac::{Hmac, Mac};
use prost::Message;
use sha2::Sha256;
#[cfg(feature = "api")]
use std::collections::BTreeMap;
use std::collections::BTreeSet;
type PeerId = u32;
include!(concat!(env!("OUT_DIR"), "/peer_rpc.rs"));
#[cfg(feature = "json-rpc")]
include!(concat!(env!("OUT_DIR"), "/peer_rpc.serde.rs"));
impl PeerGroupInfo {
pub fn generate_with_proof(group_name: String, group_secret: String, peer_id: PeerId) -> Self {
let mut mac = Hmac::<Sha256>::new_from_slice(group_secret.as_bytes())
.expect("HMAC can take key of any size");
let mut data_to_sign = group_name.as_bytes().to_vec();
data_to_sign.push(0x00); // Add a delimiter byte
data_to_sign.extend_from_slice(&peer_id.to_be_bytes());
mac.update(&data_to_sign);
let proof = mac.finalize().into_bytes().to_vec();
PeerGroupInfo {
group_name,
group_proof: proof,
}
}
pub fn verify(&self, group_secret: &str, peer_id: PeerId) -> bool {
let mut verifier = Hmac::<Sha256>::new_from_slice(group_secret.as_bytes())
.expect("HMAC can take key of any size");
let mut data_to_sign = self.group_name.as_bytes().to_vec();
data_to_sign.push(0x00); // Add a delimiter byte
data_to_sign.extend_from_slice(&peer_id.to_be_bytes());
verifier.update(&data_to_sign);
verifier.verify_slice(&self.group_proof).is_ok()
}
}
impl TrustedCredentialPubkeyProof {
pub fn generate_credential_hmac_from_bytes(
credential_bytes: &[u8],
network_secret: &str,
) -> Vec<u8> {
let mut mac = Hmac::<Sha256>::new_from_slice(network_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(b"easytier credential proof");
mac.update(credential_bytes);
mac.finalize().into_bytes().to_vec()
}
pub fn generate_credential_hmac(
credential: &TrustedCredentialPubkey,
network_secret: &str,
) -> Vec<u8> {
Self::generate_credential_hmac_from_bytes(&credential.encode_to_vec(), network_secret)
}
pub fn new_signed(credential: TrustedCredentialPubkey, network_secret: &str) -> Self {
let credential_hmac = Self::generate_credential_hmac(&credential, network_secret);
Self {
credential: Some(credential),
credential_hmac,
}
}
pub fn verify_credential_hmac(&self, network_secret: &str) -> bool {
let Some(credential) = self.credential.as_ref() else {
return false;
};
self.verify_credential_hmac_with_bytes(&credential.encode_to_vec(), network_secret)
}
pub fn verify_credential_hmac_with_bytes(
&self,
credential_bytes: &[u8],
network_secret: &str,
) -> bool {
if self.credential_hmac.is_empty() {
return false;
}
let mut mac = Hmac::<Sha256>::new_from_slice(network_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(b"easytier credential proof");
mac.update(credential_bytes);
mac.verify_slice(&self.credential_hmac).is_ok()
}
}
impl From<RouteConnBitmap> for sync_route_info_request::ConnInfo {
fn from(val: RouteConnBitmap) -> Self {
Self::ConnBitmap(val)
}
}
impl From<RouteConnPeerList> for sync_route_info_request::ConnInfo {
fn from(val: RouteConnPeerList) -> Self {
Self::ConnPeerList(val)
}
}
#[cfg(feature = "api")]
impl From<Vec<crate::api::instance::PeerInfo>> for PeerInfoForGlobalMap {
fn from(peers: Vec<crate::api::instance::PeerInfo>) -> Self {
let mut peer_map = BTreeMap::new();
for peer in peers {
let Some(min_lat) = peer
.conns
.iter()
.map(|conn| conn.stats.as_ref().unwrap().latency_us)
.min()
else {
continue;
};
let dp_info = DirectConnectedPeerInfo {
latency_ms: std::cmp::max(1, (min_lat as u32 / 1000) as i32),
};
peer_map.insert(peer.peer_id, dp_info);
}
PeerInfoForGlobalMap {
direct_peers: peer_map,
}
}
}
impl From<RoutePeerInfo> for crate::core_peer::peer::Route {
fn from(val: RoutePeerInfo) -> Self {
let network_length = if val.network_length == 0 {
24
} else {
val.network_length
};
crate::core_peer::peer::Route {
peer_id: val.peer_id,
ipv4_addr: val.ipv4_addr.map(|ipv4_addr| crate::common::Ipv4Inet {
address: Some(ipv4_addr),
network_length,
}),
next_hop_peer_id: 0,
cost: 0,
path_latency: 0,
proxy_cidrs: val.proxy_cidrs.clone(),
hostname: val.hostname.unwrap_or_default(),
stun_info: {
let mut stun_info = crate::common::StunInfo::default();
if let Ok(udp_nat_type) = crate::common::NatType::try_from(val.udp_nat_type) {
stun_info.set_udp_nat_type(udp_nat_type);
}
if let Ok(tcp_nat_type) = crate::common::NatType::try_from(val.tcp_nat_type) {
stun_info.set_tcp_nat_type(tcp_nat_type);
}
Some(stun_info)
},
inst_id: val.inst_id.map(|x| x.to_string()).unwrap_or_default(),
version: val.easytier_version,
feature_flag: val.feature_flag,
next_hop_peer_id_latency_first: None,
cost_latency_first: None,
path_latency_latency_first: None,
ipv6_addr: val.ipv6_addr,
public_ipv6_addr: val.ipv6_public_addr_lease,
ipv6_public_addr_prefix: val.ipv6_public_addr_prefix,
}
}
}
#[cfg(feature = "api")]
impl From<RoutePeerInfo> for crate::api::instance::Route {
fn from(val: RoutePeerInfo) -> Self {
let network_length = if val.network_length == 0 {
24
} else {
val.network_length
};
crate::api::instance::Route {
peer_id: val.peer_id,
ipv4_addr: val.ipv4_addr.map(|ipv4_addr| crate::common::Ipv4Inet {
address: Some(ipv4_addr),
network_length,
}),
next_hop_peer_id: 0,
cost: 0,
path_latency: 0,
proxy_cidrs: val.proxy_cidrs.clone(),
hostname: val.hostname.unwrap_or_default(),
stun_info: {
let mut stun_info = crate::common::StunInfo::default();
if let Ok(udp_nat_type) = crate::common::NatType::try_from(val.udp_nat_type) {
stun_info.set_udp_nat_type(udp_nat_type);
}
if let Ok(tcp_nat_type) = crate::common::NatType::try_from(val.tcp_nat_type) {
stun_info.set_tcp_nat_type(tcp_nat_type);
}
Some(stun_info)
},
inst_id: val.inst_id.map(|x| x.to_string()).unwrap_or_default(),
version: val.easytier_version,
feature_flag: val.feature_flag,
next_hop_peer_id_latency_first: None,
cost_latency_first: None,
path_latency_latency_first: None,
ipv6_addr: val.ipv6_addr,
public_ipv6_addr: val.ipv6_public_addr_lease,
ipv6_public_addr_prefix: val.ipv6_public_addr_prefix,
}
}
}
impl RouteConnBitmap {
pub fn get_bit(&self, idx: usize) -> bool {
let byte_idx = idx / 8;
let bit_idx = idx % 8;
let byte = self.bitmap[byte_idx];
(byte >> bit_idx) & 1 == 1
}
pub fn get_connected_peers(&self, peer_idx: usize) -> BTreeSet<PeerId> {
let mut connected_peers = BTreeSet::new();
for (idx, peer_id_version) in self.peer_ids.iter().enumerate() {
if self.get_bit(peer_idx * self.peer_ids.len() + idx) {
connected_peers.insert(peer_id_version.peer_id);
}
}
connected_peers
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peer_group_info_new() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name.clone(), group_secret, peer_id);
assert_eq!(peer_group_info.group_name, group_name);
assert!(!peer_group_info.group_proof.is_empty());
// HMAC-SHA256 produces a 32-byte output
assert_eq!(peer_group_info.group_proof.len(), 32);
}
#[test]
fn test_peer_group_info_verify_valid() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name, group_secret.clone(), peer_id);
// Verification should succeed using the same secret and peer_id
assert!(peer_group_info.verify(&group_secret, peer_id));
}
#[test]
fn test_peer_group_info_verify_invalid_secret() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let peer_group_info = PeerGroupInfo::generate_with_proof(group_name, group_secret, peer_id);
// Verification should fail with a wrong secret
assert!(!peer_group_info.verify("wrong_secret", peer_id));
}
#[test]
fn test_peer_group_info_verify_invalid_peer_id() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name, group_secret.clone(), peer_id);
// Verification should fail with a wrong peer_id
assert!(!peer_group_info.verify(&group_secret, 999u32));
}
#[test]
fn test_peer_group_info_different_groups_different_proofs() {
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let group1 =
PeerGroupInfo::generate_with_proof("group1".to_string(), group_secret.clone(), peer_id);
let group2 =
PeerGroupInfo::generate_with_proof("group2".to_string(), group_secret, peer_id);
// Different group names should produce different proofs
assert_ne!(group1.group_proof, group2.group_proof);
}
#[test]
fn test_peer_group_info_same_params_same_proof() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let group1 =
PeerGroupInfo::generate_with_proof(group_name.clone(), group_secret.clone(), peer_id);
let group2 = PeerGroupInfo::generate_with_proof(group_name, group_secret, peer_id);
// Same parameters should produce the same proof
assert_eq!(group1.group_proof, group2.group_proof);
}
#[test]
fn test_peer_group_info_empty_group_name() {
let group_name = "".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name.clone(), group_secret.clone(), peer_id);
assert_eq!(peer_group_info.group_name, group_name);
assert!(peer_group_info.verify(&group_secret, peer_id));
}
#[test]
fn test_peer_group_info_empty_secret() {
let group_name = "test_group".to_string();
let group_secret = "".to_string();
let peer_id = 42u32;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name, group_secret.clone(), peer_id);
assert!(peer_group_info.verify(&group_secret, peer_id));
}
#[test]
fn test_peer_group_info_unicode_group_name() {
let group_name = "测试组🚀".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name.clone(), group_secret.clone(), peer_id);
assert_eq!(peer_group_info.group_name, group_name);
assert!(peer_group_info.verify(&group_secret, peer_id));
}
#[test]
fn test_peer_group_info_unicode_secret() {
let group_name = "test_group".to_string();
let group_secret = "密码123🔐".to_string();
let peer_id = 42u32;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name, group_secret.clone(), peer_id);
assert!(peer_group_info.verify(&group_secret, peer_id));
}
#[test]
fn test_peer_group_info_zero_peer_id() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = 0u32;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name, group_secret.clone(), peer_id);
assert!(peer_group_info.verify(&group_secret, peer_id));
}
#[test]
fn test_peer_group_info_max_peer_id() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = u32::MAX;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name, group_secret.clone(), peer_id);
assert!(peer_group_info.verify(&group_secret, peer_id));
}
#[test]
#[ignore]
fn perf_test_generate_with_proof() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let iterations = 100000;
let start = std::time::Instant::now();
for _ in 0..iterations {
let _ = PeerGroupInfo::generate_with_proof(
group_name.clone(),
group_secret.clone(),
peer_id,
);
}
let duration = start.elapsed();
println!(
"generate_with_proof took {:?} for {} iterations",
duration, iterations
);
println!("Avg time per iteration: {:?}", duration / iterations as u32);
}
#[test]
#[ignore]
fn perf_test_verify() {
let group_name = "test_group".to_string();
let group_secret = "secret123".to_string();
let peer_id = 42u32;
let iterations = 100000;
let peer_group_info =
PeerGroupInfo::generate_with_proof(group_name, group_secret.clone(), peer_id);
let start = std::time::Instant::now();
for _ in 0..iterations {
assert!(peer_group_info.verify(&group_secret, peer_id));
}
let duration = start.elapsed();
println!("verify took {:?} for {} iterations", duration, iterations);
println!("Avg time per iteration: {:?}", duration / iterations as u32);
}
#[test]
fn test_trusted_credential_pubkey_hmac_valid() {
let credential = TrustedCredentialPubkey {
pubkey: vec![7u8; 32],
groups: vec!["ops".to_string(), "guest".to_string()],
allow_relay: true,
expiry_unix: 123456,
allowed_proxy_cidrs: vec!["10.0.0.0/24".to_string()],
reusable: Some(true),
};
let tc = TrustedCredentialPubkeyProof::new_signed(credential, "sec-1");
assert!(tc.verify_credential_hmac("sec-1"));
assert!(!tc.verify_credential_hmac("sec-2"));
}
#[test]
fn test_trusted_credential_pubkey_hmac_tampered() {
let credential = TrustedCredentialPubkey {
pubkey: vec![8u8; 32],
groups: vec!["g1".to_string()],
allow_relay: false,
expiry_unix: 1,
allowed_proxy_cidrs: vec![],
reusable: Some(true),
};
let tc = TrustedCredentialPubkeyProof::new_signed(credential, "sec-1");
let mut tampered = tc.clone();
tampered.credential.as_mut().unwrap().allow_relay = true;
assert!(!tampered.verify_credential_hmac("sec-1"));
}
#[test]
fn test_trusted_credential_pubkey_hmac_with_raw_bytes() {
let credential = TrustedCredentialPubkey {
pubkey: vec![9u8; 32],
groups: vec!["raw".to_string()],
allow_relay: true,
expiry_unix: 123456,
allowed_proxy_cidrs: vec![],
reusable: Some(true),
};
let mut raw_credential_bytes = credential.encode_to_vec();
prost::encoding::encode_key(
9999,
prost::encoding::WireType::Varint,
&mut raw_credential_bytes,
);
prost::encoding::encode_varint(42, &mut raw_credential_bytes);
let proof = TrustedCredentialPubkeyProof {
credential: Some(credential),
credential_hmac: TrustedCredentialPubkeyProof::generate_credential_hmac_from_bytes(
&raw_credential_bytes,
"sec-1",
),
};
assert!(proof.verify_credential_hmac_with_bytes(&raw_credential_bytes, "sec-1"));
assert!(!proof.verify_credential_hmac("sec-1"));
}
}
+56
View File
@@ -0,0 +1,56 @@
//! Utility functions used by generated code; this is *not* part of the crate's public API!
use bytes;
use prost;
use super::controller;
use super::descriptor;
use super::descriptor::ServiceDescriptor;
use super::error;
use super::handler;
use super::handler::Handler;
/// Efficiently decode a particular message type from a byte buffer.
pub fn decode<M>(buf: bytes::Bytes) -> error::Result<M>
where
M: prost::Message + Default,
{
let message = prost::Message::decode(buf)?;
Ok(message)
}
/// Efficiently encode a particular message into a byte buffer.
pub fn encode<M>(message: M) -> error::Result<bytes::Bytes>
where
M: prost::Message,
{
let len = prost::Message::encoded_len(&message);
let mut buf = ::bytes::BytesMut::with_capacity(len);
prost::Message::encode(&message, &mut buf)?;
Ok(buf.freeze())
}
pub async fn call_method<H, I, O>(
handler: H,
ctrl: H::Controller,
method: <H::Descriptor as descriptor::ServiceDescriptor>::Method,
input: I,
) -> super::error::Result<O>
where
H: handler::Handler,
I: prost::Message,
O: prost::Message + Default,
{
let input_bytes = encode(input)?;
let ret_msg = handler.call(ctrl, method, input_bytes).await?;
decode(ret_msg)
}
pub trait RpcClientFactory: Clone + Send + Sync + 'static {
type Descriptor: ServiceDescriptor + Default;
type ClientImpl;
type Controller: controller::Controller;
fn new(
handler: impl Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>,
) -> Self::ClientImpl;
}
+105
View File
@@ -0,0 +1,105 @@
use std::sync::{Arc, Mutex};
use bytes::Bytes;
use crate::proto::common::TunnelInfo;
// Controller must impl clone and all cloned controllers share the same data
pub trait Controller: Send + Sync + Clone + 'static {
fn timeout_ms(&self) -> i32 {
5000
}
fn set_timeout_ms(&mut self, _timeout_ms: i32) {}
fn set_trace_id(&mut self, _trace_id: i32) {}
fn trace_id(&self) -> i32 {
0
}
fn set_raw_input(&mut self, _raw_input: Bytes) {}
fn get_raw_input(&self) -> Option<Bytes> {
None
}
fn set_tunnel_info(&mut self, _tunnel_info: Option<TunnelInfo>) {}
fn get_tunnel_info(&self) -> Option<&TunnelInfo> {
None
}
fn set_raw_output(&mut self, _raw_output: Bytes) {}
fn get_raw_output(&self) -> Option<Bytes> {
None
}
}
#[derive(Debug)]
pub struct BaseControllerRawData {
pub raw_input: Option<Bytes>,
pub raw_output: Option<Bytes>,
}
#[derive(Debug, Clone)]
pub struct BaseController {
pub timeout_ms: i32,
pub trace_id: i32,
pub raw_data: Arc<Mutex<BaseControllerRawData>>,
pub tunnel_info: Option<TunnelInfo>,
}
impl Controller for BaseController {
fn timeout_ms(&self) -> i32 {
self.timeout_ms
}
fn set_timeout_ms(&mut self, timeout_ms: i32) {
self.timeout_ms = timeout_ms;
}
fn set_trace_id(&mut self, trace_id: i32) {
self.trace_id = trace_id;
}
fn trace_id(&self) -> i32 {
self.trace_id
}
fn set_raw_input(&mut self, raw_input: Bytes) {
self.raw_data.lock().unwrap().raw_input = Some(raw_input);
}
fn get_raw_input(&self) -> Option<Bytes> {
self.raw_data.lock().unwrap().raw_input.clone()
}
fn set_raw_output(&mut self, raw_output: Bytes) {
self.raw_data.lock().unwrap().raw_output = Some(raw_output);
}
fn get_raw_output(&self) -> Option<Bytes> {
self.raw_data.lock().unwrap().raw_output.clone()
}
fn get_tunnel_info(&self) -> Option<&TunnelInfo> {
self.tunnel_info.as_ref()
}
fn set_tunnel_info(&mut self, tunnel_info: Option<TunnelInfo>) {
self.tunnel_info = tunnel_info;
}
}
impl Default for BaseController {
fn default() -> Self {
Self {
timeout_ms: 5000,
trace_id: 0,
raw_data: Arc::new(Mutex::new(BaseControllerRawData {
raw_input: None,
raw_output: None,
})),
tunnel_info: None,
}
}
}
@@ -0,0 +1,50 @@
//! Traits for defining generic service descriptor definitions.
//!
//! These traits are built on the assumption that some form of code generation is being used (e.g.
//! using only `&'static str`s) but it's of course possible to implement these traits manually.
use std::any;
use std::fmt;
/// A descriptor for an available RPC service.
pub trait ServiceDescriptor: Clone + fmt::Debug + Send + Sync {
/// The associated type of method descriptors.
type Method: MethodDescriptor + fmt::Debug + TryFrom<u8>;
/// The name of the service, used in Rust code and perhaps for human readability.
fn name(&self) -> &'static str;
/// The raw protobuf name of the service.
fn proto_name(&self) -> &'static str;
/// The package name of the service.
fn package(&self) -> &'static str {
""
}
/// All of the available methods on the service.
fn methods(&self) -> &'static [Self::Method];
}
/// A descriptor for a method available on an RPC service.
pub trait MethodDescriptor: Clone + Copy + fmt::Debug + Send + Sync {
/// The name of the service, used in Rust code and perhaps for human readability.
fn name(&self) -> &'static str;
/// The raw protobuf name of the service.
fn proto_name(&self) -> &'static str;
/// The Rust `TypeId` for the input that this method accepts.
fn input_type(&self) -> any::TypeId;
/// The raw protobuf name for the input type that this method accepts.
fn input_proto_type(&self) -> &'static str;
/// The Rust `TypeId` for the output that this method produces.
fn output_type(&self) -> any::TypeId;
/// The raw protobuf name for the output type that this method produces.
fn output_proto_type(&self) -> &'static str;
/// The index of the method in the service descriptor.
fn index(&self) -> u8;
}
+49
View File
@@ -0,0 +1,49 @@
//! Error type definitions for errors that can occur during RPC interactions.
use std::result;
use prost;
use thiserror;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Rust error: {0}")]
ExecutionError(#[from] anyhow::Error),
#[error("Decode error")]
DecodeError,
#[error("Encode error")]
EncodeError,
#[error("Invalid method index: {0}, service: {1}")]
InvalidMethodIndex(u8, String),
#[error("Invalid service name: {0}, proto name: {1}")]
InvalidServiceKey(String, String),
#[error("Invalid packet: {0}")]
MalformatRpcPacket(String),
#[error("Timeout: {0}")]
Timeout(#[from] tokio::time::error::Elapsed),
#[error("Tunnel error: {0}")]
TunnelError(String),
#[error("Shutdown")]
Shutdown,
}
impl From<prost::DecodeError> for Error {
fn from(_: prost::DecodeError) -> Self {
Error::DecodeError
}
}
impl From<prost::EncodeError> for Error {
fn from(_: prost::EncodeError) -> Self {
Error::EncodeError
}
}
pub type Result<T> = result::Result<T, Error>;
+76
View File
@@ -0,0 +1,76 @@
//! Traits for defining generic RPC handlers.
use crate::proto::rpc_types::descriptor::MethodDescriptor;
use super::{
controller::Controller,
descriptor::{self, ServiceDescriptor},
};
use bytes;
/// An implementation of a specific RPC handler.
///
/// This can be an actual implementation of a service, or something that will send a request over
/// a network to fulfill a request.
#[async_trait::async_trait]
pub trait Handler: Clone + Send + Sync + 'static {
/// The service descriptor for the service whose requests this handler can handle.
type Descriptor: descriptor::ServiceDescriptor + Default;
type Controller: super::controller::Controller;
/// Perform a raw call to the specified service and method.
async fn call(
&self,
ctrl: Self::Controller,
method: <Self::Descriptor as descriptor::ServiceDescriptor>::Method,
input: bytes::Bytes,
) -> super::error::Result<bytes::Bytes>;
fn service_descriptor(&self) -> Self::Descriptor {
Self::Descriptor::default()
}
fn get_method_from_index(
&self,
index: u8,
) -> super::error::Result<<Self::Descriptor as descriptor::ServiceDescriptor>::Method> {
let desc = self.service_descriptor();
<Self::Descriptor as descriptor::ServiceDescriptor>::Method::try_from(index)
.map_err(|_| super::error::Error::InvalidMethodIndex(index, desc.name().to_string()))
}
}
#[async_trait::async_trait]
pub trait HandlerExt: Send + Sync + 'static {
type Controller;
async fn call_method(
&self,
ctrl: Self::Controller,
method_index: u8,
input: bytes::Bytes,
) -> super::error::Result<bytes::Bytes>;
fn get_method_name(&self, method_index: u8) -> super::error::Result<String>;
}
#[async_trait::async_trait]
impl<C: Controller, T: Handler<Controller = C>> HandlerExt for T {
type Controller = C;
async fn call_method(
&self,
ctrl: Self::Controller,
method_index: u8,
input: bytes::Bytes,
) -> super::error::Result<bytes::Bytes> {
let method = self.get_method_from_index(method_index)?;
self.call(ctrl, method, input).await
}
fn get_method_name(&self, method_index: u8) -> super::error::Result<String> {
let method = self.get_method_from_index(method_index)?;
let name = method.name().to_string();
Ok(name)
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod __rt;
pub mod controller;
pub mod descriptor;
pub mod error;
pub mod handler;
+2
View File
@@ -0,0 +1,2 @@
include!(concat!(env!("OUT_DIR"), "/tests.rs"));
include!(concat!(env!("OUT_DIR"), "/tests.serde.rs"));
+2
View File
@@ -0,0 +1,2 @@
include!(concat!(env!("OUT_DIR"), "/web.rs"));
include!(concat!(env!("OUT_DIR"), "/web.serde.rs"));