mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 10:14:35 +00:00
feat(credential): implement credential peer auth and trust propagation (#1968)
- add credential manager and RPC/CLI for generate/list/revoke - support credential-based Noise authentication and revocation handling - propagate trusted credential metadata through OSPF route sync - classify direct peers by auth level in session maintenance - normalize sender credential flag for legacy non-secure compatibility - add unit/integration tests for credential join, relay and revocation
This commit is contained in:
@@ -37,17 +37,18 @@ use easytier::{
|
||||
instance::{
|
||||
instance_identifier::{InstanceSelector, Selector},
|
||||
list_peer_route_pair, AclManageRpc, AclManageRpcClientFactory, ConnectorManageRpc,
|
||||
ConnectorManageRpcClientFactory, DumpRouteRequest, GetAclStatsRequest,
|
||||
GetPrometheusStatsRequest, GetStatsRequest, GetVpnPortalInfoRequest,
|
||||
GetWhitelistRequest, InstanceIdentifier, ListConnectorRequest,
|
||||
ListForeignNetworkRequest, ListGlobalForeignNetworkRequest,
|
||||
ListMappedListenerRequest, ListPeerRequest, ListPeerResponse,
|
||||
ListPortForwardRequest, ListRouteRequest, ListRouteResponse,
|
||||
ConnectorManageRpcClientFactory, CredentialManageRpc,
|
||||
CredentialManageRpcClientFactory, DumpRouteRequest, GenerateCredentialRequest,
|
||||
GetAclStatsRequest, GetPrometheusStatsRequest, GetStatsRequest,
|
||||
GetVpnPortalInfoRequest, GetWhitelistRequest, InstanceIdentifier,
|
||||
ListConnectorRequest, ListCredentialsRequest, ListForeignNetworkRequest,
|
||||
ListGlobalForeignNetworkRequest, ListMappedListenerRequest, ListPeerRequest,
|
||||
ListPeerResponse, ListPortForwardRequest, ListRouteRequest, ListRouteResponse,
|
||||
MappedListenerManageRpc, MappedListenerManageRpcClientFactory, NodeInfo,
|
||||
PeerManageRpc, PeerManageRpcClientFactory, PortForwardManageRpc,
|
||||
PortForwardManageRpcClientFactory, ShowNodeInfoRequest, StatsRpc,
|
||||
StatsRpcClientFactory, TcpProxyEntryState, TcpProxyEntryTransportType, TcpProxyRpc,
|
||||
TcpProxyRpcClientFactory, VpnPortalRpc, VpnPortalRpcClientFactory,
|
||||
PortForwardManageRpcClientFactory, RevokeCredentialRequest, ShowNodeInfoRequest,
|
||||
StatsRpc, StatsRpcClientFactory, TcpProxyEntryState, TcpProxyEntryTransportType,
|
||||
TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc, VpnPortalRpcClientFactory,
|
||||
},
|
||||
logger::{
|
||||
GetLoggerConfigRequest, LogLevel, LoggerRpc, LoggerRpcClientFactory,
|
||||
@@ -134,6 +135,8 @@ enum SubCommand {
|
||||
Stats(StatsArgs),
|
||||
#[command(about = "manage logger configuration")]
|
||||
Logger(LoggerArgs),
|
||||
#[command(about = "manage temporary credentials")]
|
||||
Credential(CredentialArgs),
|
||||
#[command(about = t!("core_clap.generate_completions").to_string())]
|
||||
GenAutocomplete { shell: ShellType },
|
||||
}
|
||||
@@ -340,6 +343,42 @@ enum LoggerSubCommand {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct CredentialArgs {
|
||||
#[command(subcommand)]
|
||||
sub_command: CredentialSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum CredentialSubCommand {
|
||||
/// Generate a new temporary credential
|
||||
Generate {
|
||||
#[arg(long, help = "TTL in seconds (required)")]
|
||||
ttl: i64,
|
||||
#[arg(long, value_delimiter = ',', help = "ACL groups (comma-separated)")]
|
||||
groups: Option<Vec<String>>,
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "false",
|
||||
help = "allow relay through this credential node"
|
||||
)]
|
||||
allow_relay: bool,
|
||||
#[arg(
|
||||
long,
|
||||
value_delimiter = ',',
|
||||
help = "allowed proxy CIDRs (comma-separated)"
|
||||
)]
|
||||
allowed_proxy_cidrs: Option<Vec<String>>,
|
||||
},
|
||||
/// Revoke a credential by its ID
|
||||
Revoke {
|
||||
#[arg(help = "credential ID (public key base64)")]
|
||||
credential_id: String,
|
||||
},
|
||||
/// List all active credentials
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct ServiceArgs {
|
||||
#[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")]
|
||||
@@ -537,6 +576,18 @@ impl CommandHandler<'_> {
|
||||
.with_context(|| "failed to get config client")?)
|
||||
}
|
||||
|
||||
async fn get_credential_client(
|
||||
&self,
|
||||
) -> Result<Box<dyn CredentialManageRpc<Controller = BaseController>>, Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.lock()
|
||||
.await
|
||||
.scoped_client::<CredentialManageRpcClientFactory<BaseController>>("".to_string())
|
||||
.await
|
||||
.with_context(|| "failed to get credential client")?)
|
||||
}
|
||||
|
||||
async fn list_peers(&self) -> Result<ListPeerResponse, Error> {
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let request = ListPeerRequest {
|
||||
@@ -1363,6 +1414,121 @@ impl CommandHandler<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_credential_generate(
|
||||
&self,
|
||||
ttl: i64,
|
||||
groups: Vec<String>,
|
||||
allow_relay: bool,
|
||||
allowed_proxy_cidrs: Vec<String>,
|
||||
) -> Result<(), Error> {
|
||||
let client = self.get_credential_client().await?;
|
||||
let request = GenerateCredentialRequest {
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
ttl_seconds: ttl,
|
||||
};
|
||||
let response = client
|
||||
.generate_credential(BaseController::default(), request)
|
||||
.await?;
|
||||
|
||||
match self.output_format {
|
||||
OutputFormat::Table => {
|
||||
println!("Credential generated successfully:");
|
||||
println!(" credential_id: {}", response.credential_id);
|
||||
println!(" credential_secret: {}", response.credential_secret);
|
||||
println!();
|
||||
println!("To use this credential on a new node:");
|
||||
println!(
|
||||
" easytier-core --network-name <name> --secure-mode --credential {}",
|
||||
response.credential_secret
|
||||
);
|
||||
}
|
||||
OutputFormat::Json => {
|
||||
let json = serde_json::to_string_pretty(&response)?;
|
||||
println!("{}", json);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_credential_revoke(&self, credential_id: &str) -> Result<(), Error> {
|
||||
let client = self.get_credential_client().await?;
|
||||
let request = RevokeCredentialRequest {
|
||||
credential_id: credential_id.to_string(),
|
||||
};
|
||||
let response = client
|
||||
.revoke_credential(BaseController::default(), request)
|
||||
.await?;
|
||||
|
||||
match self.output_format {
|
||||
OutputFormat::Table => {
|
||||
if response.success {
|
||||
println!("Credential revoked successfully");
|
||||
} else {
|
||||
println!("Credential not found");
|
||||
}
|
||||
}
|
||||
OutputFormat::Json => {
|
||||
let json = serde_json::to_string_pretty(&response)?;
|
||||
println!("{}", json);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_credential_list(&self) -> Result<(), Error> {
|
||||
let client = self.get_credential_client().await?;
|
||||
let request = ListCredentialsRequest {};
|
||||
let response = client
|
||||
.list_credentials(BaseController::default(), request)
|
||||
.await?;
|
||||
|
||||
match self.output_format {
|
||||
OutputFormat::Table => {
|
||||
if response.credentials.is_empty() {
|
||||
println!("No active credentials");
|
||||
} else {
|
||||
use tabled::{builder::Builder, settings::Style};
|
||||
let mut builder = Builder::default();
|
||||
builder.push_record(["ID", "Groups", "Relay", "Expiry", "Allowed CIDRs"]);
|
||||
for cred in &response.credentials {
|
||||
let expiry = {
|
||||
let secs = cred.expiry_unix;
|
||||
let remaining = secs
|
||||
- std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
if remaining > 0 {
|
||||
format!("{}s remaining", remaining)
|
||||
} else {
|
||||
"expired".to_string()
|
||||
}
|
||||
};
|
||||
builder.push_record([
|
||||
&cred.credential_id[..],
|
||||
&cred.groups.join(","),
|
||||
if cred.allow_relay { "yes" } else { "no" },
|
||||
&expiry,
|
||||
&cred.allowed_proxy_cidrs.join(","),
|
||||
]);
|
||||
}
|
||||
let table = builder.build().with(Style::rounded()).to_string();
|
||||
println!("{}", table);
|
||||
}
|
||||
}
|
||||
OutputFormat::Json => {
|
||||
let json = serde_json::to_string_pretty(&response)?;
|
||||
println!("{}", json);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_port_list(ports_str: &str) -> Result<Vec<String>, Error> {
|
||||
let mut ports = Vec::new();
|
||||
for port_spec in ports_str.split(',') {
|
||||
@@ -2193,6 +2359,29 @@ async fn main() -> Result<(), Error> {
|
||||
handler.handle_logger_set(level).await?;
|
||||
}
|
||||
},
|
||||
SubCommand::Credential(credential_args) => match &credential_args.sub_command {
|
||||
CredentialSubCommand::Generate {
|
||||
ttl,
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
} => {
|
||||
handler
|
||||
.handle_credential_generate(
|
||||
*ttl,
|
||||
groups.clone().unwrap_or_default(),
|
||||
*allow_relay,
|
||||
allowed_proxy_cidrs.clone().unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
CredentialSubCommand::Revoke { credential_id } => {
|
||||
handler.handle_credential_revoke(credential_id).await?;
|
||||
}
|
||||
CredentialSubCommand::List => {
|
||||
handler.handle_credential_list().await?;
|
||||
}
|
||||
},
|
||||
SubCommand::GenAutocomplete { shell } => {
|
||||
let mut cmd = Cli::command();
|
||||
if let Some(shell) = shell.to_shell() {
|
||||
|
||||
Reference in New Issue
Block a user