mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-09-26 12:41:23 +08:00
feat(acl): add group-based ACL rules and related structures (#1265)
* feat(acl): add group-based ACL rules and related structures * refactor(acl): optimize group handling with Arc and improve cache management * refactor(acl): clippy * feat(tests): add performance tests for generate_with_proof and verify methods * feat: update group_trust_map to use HashMap for more secure group proofs * refactor: refactor the logic of the trusted group getting and setting * feat(acl): support kcp/quic use group acl * feat(proxy): optimize group retrieval by IP in Kcp and Quic proxy handlers * feat(tests): add group-based ACL tree node test * always allow quic proxy traffic --------- Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn> Co-authored-by: sijie.sun <sijie.sun@smartx.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2129,6 +2129,7 @@ dependencies = [
|
|||||||
"hickory-proto",
|
"hickory-proto",
|
||||||
"hickory-resolver",
|
"hickory-resolver",
|
||||||
"hickory-server",
|
"hickory-server",
|
||||||
|
"hmac",
|
||||||
"http",
|
"http",
|
||||||
"http_req",
|
"http_req",
|
||||||
"humansize",
|
"humansize",
|
||||||
@@ -2171,6 +2172,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"service-manager",
|
"service-manager",
|
||||||
|
"sha2",
|
||||||
"smoltcp",
|
"smoltcp",
|
||||||
"socket2",
|
"socket2",
|
||||||
"stun_codec",
|
"stun_codec",
|
||||||
|
@@ -215,6 +215,8 @@ derive_builder = "0.20.2"
|
|||||||
humantime-serde = "1.1.1"
|
humantime-serde = "1.1.1"
|
||||||
multimap = "0.10.0"
|
multimap = "0.10.0"
|
||||||
version-compare = "0.2.0"
|
version-compare = "0.2.0"
|
||||||
|
hmac = "0.12.1"
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
||||||
machine-uid = "0.5.3"
|
machine-uid = "0.5.3"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, SocketAddr},
|
||||||
str::FromStr as _,
|
str::FromStr as _,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
@@ -61,6 +61,8 @@ pub struct FastLookupRule {
|
|||||||
pub dst_ip_ranges: Vec<cidr::IpCidr>,
|
pub dst_ip_ranges: Vec<cidr::IpCidr>,
|
||||||
pub src_port_ranges: Vec<(u16, u16)>,
|
pub src_port_ranges: Vec<(u16, u16)>,
|
||||||
pub dst_port_ranges: Vec<(u16, u16)>,
|
pub dst_port_ranges: Vec<(u16, u16)>,
|
||||||
|
pub source_groups: HashSet<String>,
|
||||||
|
pub destination_groups: HashSet<String>,
|
||||||
pub action: Action,
|
pub action: Action,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub stateful: bool,
|
pub stateful: bool,
|
||||||
@@ -78,6 +80,8 @@ pub struct AclCacheKey {
|
|||||||
pub dst_ip: IpAddr,
|
pub dst_ip: IpAddr,
|
||||||
pub src_port: u16,
|
pub src_port: u16,
|
||||||
pub dst_port: u16,
|
pub dst_port: u16,
|
||||||
|
pub src_groups: Arc<Vec<String>>,
|
||||||
|
pub dst_groups: Arc<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AclCacheKey {
|
impl AclCacheKey {
|
||||||
@@ -89,6 +93,8 @@ impl AclCacheKey {
|
|||||||
dst_ip: packet_info.dst_ip,
|
dst_ip: packet_info.dst_ip,
|
||||||
src_port: packet_info.src_port.unwrap_or(0),
|
src_port: packet_info.src_port.unwrap_or(0),
|
||||||
dst_port: packet_info.dst_port.unwrap_or(0),
|
dst_port: packet_info.dst_port.unwrap_or(0),
|
||||||
|
src_groups: packet_info.src_groups.clone(),
|
||||||
|
dst_groups: packet_info.dst_groups.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +122,8 @@ pub struct PacketInfo {
|
|||||||
pub dst_port: Option<u16>,
|
pub dst_port: Option<u16>,
|
||||||
pub protocol: Protocol,
|
pub protocol: Protocol,
|
||||||
pub packet_size: usize,
|
pub packet_size: usize,
|
||||||
|
pub src_groups: Arc<Vec<String>>,
|
||||||
|
pub dst_groups: Arc<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACL processing result
|
// ACL processing result
|
||||||
@@ -684,6 +692,28 @@ impl AclProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source group check
|
||||||
|
if !rule.source_groups.is_empty() {
|
||||||
|
let matches = packet_info
|
||||||
|
.src_groups
|
||||||
|
.iter()
|
||||||
|
.any(|group| rule.source_groups.contains(group));
|
||||||
|
if !matches {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destination group check
|
||||||
|
if !rule.destination_groups.is_empty() {
|
||||||
|
let matches = packet_info
|
||||||
|
.dst_groups
|
||||||
|
.iter()
|
||||||
|
.any(|group| rule.destination_groups.contains(group));
|
||||||
|
if !matches {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,6 +834,8 @@ impl AclProcessor {
|
|||||||
dst_ip_ranges,
|
dst_ip_ranges,
|
||||||
src_port_ranges,
|
src_port_ranges,
|
||||||
dst_port_ranges,
|
dst_port_ranges,
|
||||||
|
source_groups: rule.source_groups.iter().cloned().collect(),
|
||||||
|
destination_groups: rule.destination_groups.iter().cloned().collect(),
|
||||||
action: rule.action(),
|
action: rule.action(),
|
||||||
enabled: rule.enabled,
|
enabled: rule.enabled,
|
||||||
stateful: rule.stateful,
|
stateful: rule.stateful,
|
||||||
@@ -1071,6 +1103,8 @@ impl AclRuleBuilder {
|
|||||||
rate_limit: 0,
|
rate_limit: 0,
|
||||||
burst_limit: 0,
|
burst_limit: 0,
|
||||||
stateful: true,
|
stateful: true,
|
||||||
|
source_groups: vec![],
|
||||||
|
destination_groups: vec![],
|
||||||
};
|
};
|
||||||
inbound_chain.rules.push(tcp_rule);
|
inbound_chain.rules.push(tcp_rule);
|
||||||
rule_priority -= 1;
|
rule_priority -= 1;
|
||||||
@@ -1093,6 +1127,8 @@ impl AclRuleBuilder {
|
|||||||
rate_limit: 0,
|
rate_limit: 0,
|
||||||
burst_limit: 0,
|
burst_limit: 0,
|
||||||
stateful: false,
|
stateful: false,
|
||||||
|
source_groups: vec![],
|
||||||
|
destination_groups: vec![],
|
||||||
};
|
};
|
||||||
inbound_chain.rules.push(udp_rule);
|
inbound_chain.rules.push(udp_rule);
|
||||||
}
|
}
|
||||||
@@ -1108,6 +1144,10 @@ impl AclRuleBuilder {
|
|||||||
} else {
|
} else {
|
||||||
acl.acl_v1 = Some(AclV1 {
|
acl.acl_v1 = Some(AclV1 {
|
||||||
chains: vec![inbound_chain],
|
chains: vec![inbound_chain],
|
||||||
|
group: Some(GroupInfo {
|
||||||
|
declares: vec![],
|
||||||
|
members: vec![],
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,6 +1184,106 @@ mod tests {
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_group_based_acl_rules() {
|
||||||
|
let mut acl_config = Acl::default();
|
||||||
|
let mut acl_v1 = AclV1::default();
|
||||||
|
let mut chain = Chain {
|
||||||
|
name: "group_test_chain".to_string(),
|
||||||
|
chain_type: ChainType::Inbound as i32,
|
||||||
|
enabled: true,
|
||||||
|
default_action: Action::Drop as i32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rules
|
||||||
|
chain.rules.push(Rule {
|
||||||
|
name: "allow_admins_to_db".to_string(),
|
||||||
|
priority: 100,
|
||||||
|
enabled: true,
|
||||||
|
action: Action::Allow as i32,
|
||||||
|
protocol: Protocol::Any as i32,
|
||||||
|
source_groups: vec!["admin".to_string()],
|
||||||
|
destination_groups: vec!["db-server".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
chain.rules.push(Rule {
|
||||||
|
name: "allow_devs_from_anywhere".to_string(),
|
||||||
|
priority: 90,
|
||||||
|
enabled: true,
|
||||||
|
action: Action::Allow as i32,
|
||||||
|
protocol: Protocol::Any as i32,
|
||||||
|
source_groups: vec!["dev".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
chain.rules.push(Rule {
|
||||||
|
name: "deny_guests_to_db".to_string(),
|
||||||
|
priority: 80,
|
||||||
|
enabled: true,
|
||||||
|
action: Action::Drop as i32,
|
||||||
|
protocol: Protocol::Any as i32,
|
||||||
|
source_groups: vec!["guest".to_string()],
|
||||||
|
destination_groups: vec!["db-server".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
chain.rules.push(Rule {
|
||||||
|
name: "allow_specific_ip".to_string(),
|
||||||
|
priority: 70,
|
||||||
|
enabled: true,
|
||||||
|
action: Action::Allow as i32,
|
||||||
|
protocol: Protocol::Any as i32,
|
||||||
|
source_ips: vec!["1.2.3.4/32".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
acl_v1.chains.push(chain);
|
||||||
|
acl_config.acl_v1 = Some(acl_v1);
|
||||||
|
|
||||||
|
let processor = AclProcessor::new(acl_config);
|
||||||
|
|
||||||
|
// Case 3.1: Source group match (devs from anywhere)
|
||||||
|
let mut packet_info = create_test_packet_info();
|
||||||
|
packet_info.src_groups = Arc::new(vec!["dev".to_string()]);
|
||||||
|
let result = processor.process_packet(&packet_info, ChainType::Inbound);
|
||||||
|
assert_eq!(result.action, Action::Allow);
|
||||||
|
assert_eq!(result.matched_rule, Some(RuleId::Priority(90)));
|
||||||
|
|
||||||
|
// Case 3.2: Source group no match
|
||||||
|
packet_info.src_groups = Arc::new(vec!["guest".to_string()]);
|
||||||
|
let result = processor.process_packet(&packet_info, ChainType::Inbound);
|
||||||
|
assert_eq!(result.action, Action::Drop); // Default drop
|
||||||
|
assert_eq!(result.matched_rule, Some(RuleId::Default));
|
||||||
|
|
||||||
|
// Case 3.3: Destination group match (deny guests to db)
|
||||||
|
packet_info.src_groups = Arc::new(vec!["guest".to_string()]);
|
||||||
|
packet_info.dst_groups = Arc::new(vec!["db-server".to_string()]);
|
||||||
|
let result = processor.process_packet(&packet_info, ChainType::Inbound);
|
||||||
|
assert_eq!(result.action, Action::Drop);
|
||||||
|
assert_eq!(result.matched_rule, Some(RuleId::Priority(80)));
|
||||||
|
|
||||||
|
// Case 3.4: Source and Destination groups match
|
||||||
|
packet_info.src_groups = Arc::new(vec!["admin".to_string()]);
|
||||||
|
packet_info.dst_groups = Arc::new(vec!["db-server".to_string()]);
|
||||||
|
let result = processor.process_packet(&packet_info, ChainType::Inbound);
|
||||||
|
assert_eq!(result.action, Action::Allow);
|
||||||
|
assert_eq!(result.matched_rule, Some(RuleId::Priority(100)));
|
||||||
|
|
||||||
|
// Case 3.5: Partial match (admin to web-server)
|
||||||
|
packet_info.src_groups = Arc::new(vec!["admin".to_string()]);
|
||||||
|
packet_info.dst_groups = Arc::new(vec!["web-server".to_string()]);
|
||||||
|
let result = processor.process_packet(&packet_info, ChainType::Inbound);
|
||||||
|
assert_eq!(result.action, Action::Drop); // Default drop
|
||||||
|
assert_eq!(result.matched_rule, Some(RuleId::Default));
|
||||||
|
|
||||||
|
// Case 3.6: Rule with no group definition
|
||||||
|
packet_info.src_ip = "1.2.3.4".parse().unwrap();
|
||||||
|
packet_info.src_groups = Arc::new(vec!["admin".to_string()]);
|
||||||
|
packet_info.dst_groups = Arc::new(vec![]);
|
||||||
|
let result = processor.process_packet(&packet_info, ChainType::Inbound);
|
||||||
|
assert_eq!(result.action, Action::Allow);
|
||||||
|
assert_eq!(result.matched_rule, Some(RuleId::Priority(70)));
|
||||||
|
}
|
||||||
|
|
||||||
fn create_test_acl_config() -> Acl {
|
fn create_test_acl_config() -> Acl {
|
||||||
let mut acl_config = Acl::default();
|
let mut acl_config = Acl::default();
|
||||||
|
|
||||||
@@ -1182,6 +1322,8 @@ mod tests {
|
|||||||
dst_port: Some(80),
|
dst_port: Some(80),
|
||||||
protocol: Protocol::Tcp,
|
protocol: Protocol::Tcp,
|
||||||
packet_size: 1024,
|
packet_size: 1024,
|
||||||
|
src_groups: Arc::new(vec![]),
|
||||||
|
dst_groups: Arc::new(vec![]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1380,6 +1522,8 @@ mod tests {
|
|||||||
dst_port: Some(53), // DNS
|
dst_port: Some(53), // DNS
|
||||||
protocol: Protocol::Udp, // UDP
|
protocol: Protocol::Udp, // UDP
|
||||||
packet_size: 512,
|
packet_size: 512,
|
||||||
|
src_groups: Arc::new(vec![]),
|
||||||
|
dst_groups: Arc::new(vec![]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test TCP packet (should hit stateful+rate-limited rule)
|
// Test TCP packet (should hit stateful+rate-limited rule)
|
||||||
@@ -1390,6 +1534,8 @@ mod tests {
|
|||||||
dst_port: Some(80), // HTTP
|
dst_port: Some(80), // HTTP
|
||||||
protocol: Protocol::Tcp, // TCP
|
protocol: Protocol::Tcp, // TCP
|
||||||
packet_size: 1024,
|
packet_size: 1024,
|
||||||
|
src_groups: Arc::new(vec![]),
|
||||||
|
dst_groups: Arc::new(vec![]),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process UDP packets multiple times
|
// Process UDP packets multiple times
|
||||||
|
@@ -8,8 +8,10 @@ use crate::common::config::ProxyNetworkConfig;
|
|||||||
use crate::common::stats_manager::StatsManager;
|
use crate::common::stats_manager::StatsManager;
|
||||||
use crate::common::token_bucket::TokenBucketManager;
|
use crate::common::token_bucket::TokenBucketManager;
|
||||||
use crate::peers::acl_filter::AclFilter;
|
use crate::peers::acl_filter::AclFilter;
|
||||||
|
use crate::proto::acl::GroupIdentity;
|
||||||
use crate::proto::cli::PeerConnInfo;
|
use crate::proto::cli::PeerConnInfo;
|
||||||
use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb};
|
use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb};
|
||||||
|
use crate::proto::peer_rpc::PeerGroupInfo;
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -351,6 +353,7 @@ impl GlobalCtx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_quic_proxy_port(&self, port: Option<u16>) {
|
pub fn set_quic_proxy_port(&self, port: Option<u16>) {
|
||||||
|
self.acl_filter.set_quic_udp_port(port.unwrap_or(0));
|
||||||
self.quic_proxy_port.store(port);
|
self.quic_proxy_port.store(port);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +368,37 @@ impl GlobalCtx {
|
|||||||
pub fn get_acl_filter(&self) -> &Arc<AclFilter> {
|
pub fn get_acl_filter(&self) -> &Arc<AclFilter> {
|
||||||
&self.acl_filter
|
&self.acl_filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_acl_groups(&self, peer_id: PeerId) -> Vec<PeerGroupInfo> {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
self.config
|
||||||
|
.get_acl()
|
||||||
|
.and_then(|acl| acl.acl_v1)
|
||||||
|
.and_then(|acl_v1| acl_v1.group)
|
||||||
|
.map_or_else(Vec::new, |group| {
|
||||||
|
let memberships: HashSet<_> = group.members.iter().collect();
|
||||||
|
group
|
||||||
|
.declares
|
||||||
|
.iter()
|
||||||
|
.filter(|g| memberships.contains(&g.group_name))
|
||||||
|
.map(|g| {
|
||||||
|
PeerGroupInfo::generate_with_proof(
|
||||||
|
g.group_name.clone(),
|
||||||
|
g.group_secret.clone(),
|
||||||
|
peer_id,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_acl_group_declarations(&self) -> Vec<GroupIdentity> {
|
||||||
|
self.config
|
||||||
|
.get_acl()
|
||||||
|
.and_then(|acl| acl.acl_v1)
|
||||||
|
.and_then(|acl_v1| acl_v1.group)
|
||||||
|
.map_or_else(Vec::new, |group| group.declares.to_vec())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@@ -440,12 +440,13 @@ impl KcpProxyDst {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(ret)]
|
#[tracing::instrument(ret, skip(route))]
|
||||||
async fn handle_one_in_stream(
|
async fn handle_one_in_stream(
|
||||||
kcp_stream: KcpStream,
|
kcp_stream: KcpStream,
|
||||||
global_ctx: ArcGlobalCtx,
|
global_ctx: ArcGlobalCtx,
|
||||||
proxy_entries: Arc<DashMap<ConnId, TcpProxyEntry>>,
|
proxy_entries: Arc<DashMap<ConnId, TcpProxyEntry>>,
|
||||||
cidr_set: Arc<CidrSet>,
|
cidr_set: Arc<CidrSet>,
|
||||||
|
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut conn_data = kcp_stream.conn_data().clone();
|
let mut conn_data = kcp_stream.conn_data().clone();
|
||||||
let parsed_conn_data = KcpConnData::decode(&mut conn_data)
|
let parsed_conn_data = KcpConnData::decode(&mut conn_data)
|
||||||
@@ -481,6 +482,13 @@ impl KcpProxyDst {
|
|||||||
proxy_entries.remove(&conn_id);
|
proxy_entries.remove(&conn_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let src_ip = src_socket.ip();
|
||||||
|
let dst_ip = dst_socket.ip();
|
||||||
|
let (src_groups, dst_groups) = tokio::join!(
|
||||||
|
route.get_peer_groups_by_ip(&src_ip),
|
||||||
|
route.get_peer_groups_by_ip(&dst_ip)
|
||||||
|
);
|
||||||
|
|
||||||
let send_to_self =
|
let send_to_self =
|
||||||
Some(dst_socket.ip()) == global_ctx.get_ipv4().map(|ip| IpAddr::V4(ip.address()));
|
Some(dst_socket.ip()) == global_ctx.get_ipv4().map(|ip| IpAddr::V4(ip.address()));
|
||||||
|
|
||||||
@@ -491,12 +499,14 @@ impl KcpProxyDst {
|
|||||||
let acl_handler = ProxyAclHandler {
|
let acl_handler = ProxyAclHandler {
|
||||||
acl_filter: global_ctx.get_acl_filter().clone(),
|
acl_filter: global_ctx.get_acl_filter().clone(),
|
||||||
packet_info: PacketInfo {
|
packet_info: PacketInfo {
|
||||||
src_ip: src_socket.ip(),
|
src_ip,
|
||||||
dst_ip: dst_socket.ip(),
|
dst_ip,
|
||||||
src_port: Some(src_socket.port()),
|
src_port: Some(src_socket.port()),
|
||||||
dst_port: Some(dst_socket.port()),
|
dst_port: Some(dst_socket.port()),
|
||||||
protocol: Protocol::Tcp,
|
protocol: Protocol::Tcp,
|
||||||
packet_size: conn_data.len(),
|
packet_size: conn_data.len(),
|
||||||
|
src_groups,
|
||||||
|
dst_groups,
|
||||||
},
|
},
|
||||||
chain_type: if send_to_self {
|
chain_type: if send_to_self {
|
||||||
ChainType::Inbound
|
ChainType::Inbound
|
||||||
@@ -530,6 +540,7 @@ impl KcpProxyDst {
|
|||||||
let global_ctx = self.peer_manager.get_global_ctx().clone();
|
let global_ctx = self.peer_manager.get_global_ctx().clone();
|
||||||
let proxy_entries = self.proxy_entries.clone();
|
let proxy_entries = self.proxy_entries.clone();
|
||||||
let cidr_set = self.cidr_set.clone();
|
let cidr_set = self.cidr_set.clone();
|
||||||
|
let route = Arc::new(self.peer_manager.get_route());
|
||||||
self.tasks.spawn(async move {
|
self.tasks.spawn(async move {
|
||||||
while let Ok(conn) = kcp_endpoint.accept().await {
|
while let Ok(conn) = kcp_endpoint.accept().await {
|
||||||
let stream = KcpStream::new(&kcp_endpoint, conn)
|
let stream = KcpStream::new(&kcp_endpoint, conn)
|
||||||
@@ -539,9 +550,16 @@ impl KcpProxyDst {
|
|||||||
let global_ctx = global_ctx.clone();
|
let global_ctx = global_ctx.clone();
|
||||||
let proxy_entries = proxy_entries.clone();
|
let proxy_entries = proxy_entries.clone();
|
||||||
let cidr_set = cidr_set.clone();
|
let cidr_set = cidr_set.clone();
|
||||||
|
let route = route.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = Self::handle_one_in_stream(stream, global_ctx, proxy_entries, cidr_set)
|
let _ = Self::handle_one_in_stream(
|
||||||
.await;
|
stream,
|
||||||
|
global_ctx,
|
||||||
|
proxy_entries,
|
||||||
|
cidr_set,
|
||||||
|
route,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -247,10 +247,14 @@ pub struct QUICProxyDst {
|
|||||||
endpoint: Arc<quinn::Endpoint>,
|
endpoint: Arc<quinn::Endpoint>,
|
||||||
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
||||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||||
|
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QUICProxyDst {
|
impl QUICProxyDst {
|
||||||
pub fn new(global_ctx: ArcGlobalCtx) -> Result<Self> {
|
pub fn new(
|
||||||
|
global_ctx: ArcGlobalCtx,
|
||||||
|
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
||||||
|
) -> Result<Self> {
|
||||||
let _g = global_ctx.net_ns.guard();
|
let _g = global_ctx.net_ns.guard();
|
||||||
let (endpoint, _) = make_server_endpoint("0.0.0.0:0".parse().unwrap())
|
let (endpoint, _) = make_server_endpoint("0.0.0.0:0".parse().unwrap())
|
||||||
.map_err(|e| anyhow::anyhow!("failed to create QUIC endpoint: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("failed to create QUIC endpoint: {}", e))?;
|
||||||
@@ -261,6 +265,7 @@ impl QUICProxyDst {
|
|||||||
endpoint: Arc::new(endpoint),
|
endpoint: Arc::new(endpoint),
|
||||||
proxy_entries: Arc::new(DashMap::new()),
|
proxy_entries: Arc::new(DashMap::new()),
|
||||||
tasks,
|
tasks,
|
||||||
|
route,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +275,7 @@ impl QUICProxyDst {
|
|||||||
let ctx = self.global_ctx.clone();
|
let ctx = self.global_ctx.clone();
|
||||||
let cidr_set = Arc::new(CidrSet::new(ctx.clone()));
|
let cidr_set = Arc::new(CidrSet::new(ctx.clone()));
|
||||||
let proxy_entries = self.proxy_entries.clone();
|
let proxy_entries = self.proxy_entries.clone();
|
||||||
|
let route = self.route.clone();
|
||||||
|
|
||||||
let task = async move {
|
let task = async move {
|
||||||
loop {
|
loop {
|
||||||
@@ -289,6 +295,7 @@ impl QUICProxyDst {
|
|||||||
ctx.clone(),
|
ctx.clone(),
|
||||||
cidr_set.clone(),
|
cidr_set.clone(),
|
||||||
proxy_entries.clone(),
|
proxy_entries.clone(),
|
||||||
|
route.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -312,6 +319,7 @@ impl QUICProxyDst {
|
|||||||
ctx: Arc<GlobalCtx>,
|
ctx: Arc<GlobalCtx>,
|
||||||
cidr_set: Arc<CidrSet>,
|
cidr_set: Arc<CidrSet>,
|
||||||
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
||||||
|
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
||||||
) {
|
) {
|
||||||
let remote_addr = conn.remote_address();
|
let remote_addr = conn.remote_address();
|
||||||
defer!(
|
defer!(
|
||||||
@@ -319,7 +327,14 @@ impl QUICProxyDst {
|
|||||||
);
|
);
|
||||||
let ret = timeout(
|
let ret = timeout(
|
||||||
std::time::Duration::from_secs(10),
|
std::time::Duration::from_secs(10),
|
||||||
Self::handle_connection(conn, ctx, cidr_set, remote_addr, proxy_entries.clone()),
|
Self::handle_connection(
|
||||||
|
conn,
|
||||||
|
ctx,
|
||||||
|
cidr_set,
|
||||||
|
remote_addr,
|
||||||
|
proxy_entries.clone(),
|
||||||
|
route,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -348,6 +363,7 @@ impl QUICProxyDst {
|
|||||||
cidr_set: Arc<CidrSet>,
|
cidr_set: Arc<CidrSet>,
|
||||||
proxy_entry_key: SocketAddr,
|
proxy_entry_key: SocketAddr,
|
||||||
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
||||||
|
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
||||||
) -> Result<(QUICStream, TcpStream, ProxyAclHandler)> {
|
) -> Result<(QUICStream, TcpStream, ProxyAclHandler)> {
|
||||||
let conn = incoming.await.with_context(|| "accept failed")?;
|
let conn = incoming.await.with_context(|| "accept failed")?;
|
||||||
let addr = conn.remote_address();
|
let addr = conn.remote_address();
|
||||||
@@ -379,6 +395,13 @@ impl QUICProxyDst {
|
|||||||
dst_socket.set_ip(real_ip);
|
dst_socket.set_ip(real_ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let src_ip = addr.ip();
|
||||||
|
let dst_ip = *dst_socket.ip();
|
||||||
|
let (src_groups, dst_groups) = tokio::join!(
|
||||||
|
route.get_peer_groups_by_ip(&src_ip),
|
||||||
|
route.get_peer_groups_by_ipv4(&dst_ip)
|
||||||
|
);
|
||||||
|
|
||||||
let send_to_self = Some(*dst_socket.ip()) == ctx.get_ipv4().map(|ip| ip.address());
|
let send_to_self = Some(*dst_socket.ip()) == ctx.get_ipv4().map(|ip| ip.address());
|
||||||
if send_to_self && ctx.no_tun() {
|
if send_to_self && ctx.no_tun() {
|
||||||
dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse().unwrap();
|
dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse().unwrap();
|
||||||
@@ -398,12 +421,14 @@ impl QUICProxyDst {
|
|||||||
let acl_handler = ProxyAclHandler {
|
let acl_handler = ProxyAclHandler {
|
||||||
acl_filter: ctx.get_acl_filter().clone(),
|
acl_filter: ctx.get_acl_filter().clone(),
|
||||||
packet_info: PacketInfo {
|
packet_info: PacketInfo {
|
||||||
src_ip: addr.ip(),
|
src_ip,
|
||||||
dst_ip: (*dst_socket.ip()).into(),
|
dst_ip: dst_ip.into(),
|
||||||
src_port: Some(addr.port()),
|
src_port: Some(addr.port()),
|
||||||
dst_port: Some(dst_socket.port()),
|
dst_port: Some(dst_socket.port()),
|
||||||
protocol: Protocol::Tcp,
|
protocol: Protocol::Tcp,
|
||||||
packet_size: len as usize,
|
packet_size: len as usize,
|
||||||
|
src_groups,
|
||||||
|
dst_groups,
|
||||||
},
|
},
|
||||||
chain_type: if send_to_self {
|
chain_type: if send_to_self {
|
||||||
ChainType::Inbound
|
ChainType::Inbound
|
||||||
|
@@ -531,7 +531,8 @@ impl Instance {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let quic_dst = QUICProxyDst::new(self.global_ctx.clone())?;
|
let route = Arc::new(self.peer_manager.get_route());
|
||||||
|
let quic_dst = QUICProxyDst::new(self.global_ctx.clone(), route)?;
|
||||||
quic_dst.start().await?;
|
quic_dst.start().await?;
|
||||||
self.global_ctx
|
self.global_ctx
|
||||||
.set_quic_proxy_port(Some(quic_dst.local_addr()?.port()));
|
.set_quic_proxy_port(Some(quic_dst.local_addr()?.port()));
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::{AtomicU16, Ordering};
|
||||||
use std::{
|
use std::{
|
||||||
net::IpAddr,
|
net::IpAddr,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
@@ -25,6 +25,7 @@ pub struct AclFilter {
|
|||||||
// Use ArcSwap for lock-free atomic replacement during hot reload
|
// Use ArcSwap for lock-free atomic replacement during hot reload
|
||||||
acl_processor: ArcSwap<AclProcessor>,
|
acl_processor: ArcSwap<AclProcessor>,
|
||||||
acl_enabled: Arc<AtomicBool>,
|
acl_enabled: Arc<AtomicBool>,
|
||||||
|
quic_udp_port: AtomicU16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AclFilter {
|
impl Default for AclFilter {
|
||||||
@@ -38,6 +39,7 @@ impl AclFilter {
|
|||||||
Self {
|
Self {
|
||||||
acl_processor: ArcSwap::from(Arc::new(AclProcessor::new(Acl::default()))),
|
acl_processor: ArcSwap::from(Arc::new(AclProcessor::new(Acl::default()))),
|
||||||
acl_enabled: Arc::new(AtomicBool::new(false)),
|
acl_enabled: Arc::new(AtomicBool::new(false)),
|
||||||
|
quic_udp_port: AtomicU16::new(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +90,11 @@ impl AclFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extract packet information for ACL processing
|
/// Extract packet information for ACL processing
|
||||||
fn extract_packet_info(&self, packet: &ZCPacket) -> Option<PacketInfo> {
|
fn extract_packet_info(
|
||||||
|
&self,
|
||||||
|
packet: &ZCPacket,
|
||||||
|
route: &(dyn super::route_trait::Route + Send + Sync + 'static),
|
||||||
|
) -> Option<PacketInfo> {
|
||||||
let payload = packet.payload();
|
let payload = packet.payload();
|
||||||
|
|
||||||
let src_ip;
|
let src_ip;
|
||||||
@@ -155,6 +161,15 @@ impl AclFilter {
|
|||||||
_ => Protocol::Unspecified,
|
_ => Protocol::Unspecified,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let src_groups = packet
|
||||||
|
.get_src_peer_id()
|
||||||
|
.map(|peer_id| route.get_peer_groups(peer_id))
|
||||||
|
.unwrap_or_else(|| Arc::new(Vec::new()));
|
||||||
|
let dst_groups = packet
|
||||||
|
.get_dst_peer_id()
|
||||||
|
.map(|peer_id| route.get_peer_groups(peer_id))
|
||||||
|
.unwrap_or_else(|| Arc::new(Vec::new()));
|
||||||
|
|
||||||
Some(PacketInfo {
|
Some(PacketInfo {
|
||||||
src_ip,
|
src_ip,
|
||||||
dst_ip,
|
dst_ip,
|
||||||
@@ -162,6 +177,8 @@ impl AclFilter {
|
|||||||
dst_port,
|
dst_port,
|
||||||
protocol: acl_protocol,
|
protocol: acl_protocol,
|
||||||
packet_size: payload.len(),
|
packet_size: payload.len(),
|
||||||
|
src_groups,
|
||||||
|
dst_groups,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +198,8 @@ impl AclFilter {
|
|||||||
dst_ip = %packet_info.dst_ip,
|
dst_ip = %packet_info.dst_ip,
|
||||||
src_port = packet_info.src_port,
|
src_port = packet_info.src_port,
|
||||||
dst_port = packet_info.dst_port,
|
dst_port = packet_info.dst_port,
|
||||||
|
src_group = packet_info.src_groups.join(","),
|
||||||
|
dst_group = packet_info.dst_groups.join(","),
|
||||||
protocol = ?packet_info.protocol,
|
protocol = ?packet_info.protocol,
|
||||||
action = ?result.action,
|
action = ?result.action,
|
||||||
rule = result.matched_rule_str().as_deref().unwrap_or("unknown"),
|
rule = result.matched_rule_str().as_deref().unwrap_or("unknown"),
|
||||||
@@ -226,6 +245,40 @@ impl AclFilter {
|
|||||||
processor.increment_stat(AclStatKey::PacketsTotal);
|
processor.increment_stat(AclStatKey::PacketsTotal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_is_quic_packet(
|
||||||
|
&self,
|
||||||
|
packet_info: &PacketInfo,
|
||||||
|
my_ipv4: &Option<Ipv4Addr>,
|
||||||
|
my_ipv6: &Option<Ipv6Addr>,
|
||||||
|
) -> bool {
|
||||||
|
if packet_info.protocol != Protocol::Udp {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quic_port = self.get_quic_udp_port();
|
||||||
|
if quic_port == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// quic input
|
||||||
|
if packet_info.dst_port == Some(quic_port)
|
||||||
|
&& (packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|
||||||
|
|| packet_info.dst_ip == my_ipv6.unwrap_or(Ipv6Addr::UNSPECIFIED))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// quic output
|
||||||
|
if packet_info.src_port == Some(quic_port)
|
||||||
|
&& (packet_info.src_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|
||||||
|
|| packet_info.src_ip == my_ipv6.unwrap_or(Ipv6Addr::UNSPECIFIED))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Common ACL processing logic
|
/// Common ACL processing logic
|
||||||
pub fn process_packet_with_acl(
|
pub fn process_packet_with_acl(
|
||||||
&self,
|
&self,
|
||||||
@@ -233,6 +286,7 @@ impl AclFilter {
|
|||||||
is_in: bool,
|
is_in: bool,
|
||||||
my_ipv4: Option<Ipv4Addr>,
|
my_ipv4: Option<Ipv4Addr>,
|
||||||
my_ipv6: Option<Ipv6Addr>,
|
my_ipv6: Option<Ipv6Addr>,
|
||||||
|
route: &(dyn super::route_trait::Route + Send + Sync + 'static),
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if !self.acl_enabled.load(Ordering::Relaxed) {
|
if !self.acl_enabled.load(Ordering::Relaxed) {
|
||||||
return true;
|
return true;
|
||||||
@@ -243,7 +297,7 @@ impl AclFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract packet information
|
// Extract packet information
|
||||||
let packet_info = match self.extract_packet_info(packet) {
|
let packet_info = match self.extract_packet_info(packet, route) {
|
||||||
Some(info) => info,
|
Some(info) => info,
|
||||||
None => {
|
None => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -256,6 +310,10 @@ impl AclFilter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if self.check_is_quic_packet(&packet_info, &my_ipv4, &my_ipv6) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
let chain_type = if is_in {
|
let chain_type = if is_in {
|
||||||
if packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|
if packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|
||||||
|| packet_info.dst_ip == my_ipv6.unwrap_or(Ipv6Addr::UNSPECIFIED)
|
|| packet_info.dst_ip == my_ipv6.unwrap_or(Ipv6Addr::UNSPECIFIED)
|
||||||
@@ -292,4 +350,12 @@ impl AclFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_quic_udp_port(&self) -> u16 {
|
||||||
|
self.quic_udp_port.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_quic_udp_port(&self, port: u16) {
|
||||||
|
self.quic_udp_port.store(port, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,7 @@ use crate::{
|
|||||||
peer_conn::PeerConn,
|
peer_conn::PeerConn,
|
||||||
peer_rpc::PeerRpcManagerTransport,
|
peer_rpc::PeerRpcManagerTransport,
|
||||||
recv_packet_from_chan,
|
recv_packet_from_chan,
|
||||||
route_trait::{ForeignNetworkRouteInfoMap, NextHopPolicy, RouteInterface},
|
route_trait::{ForeignNetworkRouteInfoMap, MockRoute, NextHopPolicy, RouteInterface},
|
||||||
PeerPacketFilter,
|
PeerPacketFilter,
|
||||||
},
|
},
|
||||||
proto::{
|
proto::{
|
||||||
@@ -634,6 +634,7 @@ impl PeerManager {
|
|||||||
let acl_filter = self.global_ctx.get_acl_filter().clone();
|
let acl_filter = self.global_ctx.get_acl_filter().clone();
|
||||||
let global_ctx = self.global_ctx.clone();
|
let global_ctx = self.global_ctx.clone();
|
||||||
let stats_mgr = self.global_ctx.stats_manager().clone();
|
let stats_mgr = self.global_ctx.stats_manager().clone();
|
||||||
|
let route = self.get_route();
|
||||||
|
|
||||||
let label_set =
|
let label_set =
|
||||||
LabelSet::new().with_label_type(LabelType::NetworkName(global_ctx.get_network_name()));
|
LabelSet::new().with_label_type(LabelType::NetworkName(global_ctx.get_network_name()));
|
||||||
@@ -737,6 +738,7 @@ impl PeerManager {
|
|||||||
true,
|
true,
|
||||||
global_ctx.get_ipv4().map(|x| x.address()),
|
global_ctx.get_ipv4().map(|x| x.address()),
|
||||||
global_ctx.get_ipv6().map(|x| x.address()),
|
global_ctx.get_ipv6().map(|x| x.address()),
|
||||||
|
&route,
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -914,7 +916,7 @@ impl PeerManager {
|
|||||||
pub fn get_route(&self) -> Box<dyn Route + Send + Sync + 'static> {
|
pub fn get_route(&self) -> Box<dyn Route + Send + Sync + 'static> {
|
||||||
match &self.route_algo_inst {
|
match &self.route_algo_inst {
|
||||||
RouteAlgoInst::Ospf(route) => Box::new(route.clone()),
|
RouteAlgoInst::Ospf(route) => Box::new(route.clone()),
|
||||||
RouteAlgoInst::None => panic!("no route"),
|
RouteAlgoInst::None => Box::new(MockRoute {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -960,11 +962,13 @@ impl PeerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_nic_packet_process_pipeline(&self, data: &mut ZCPacket) {
|
async fn run_nic_packet_process_pipeline(&self, data: &mut ZCPacket) {
|
||||||
if !self
|
if !self.global_ctx.get_acl_filter().process_packet_with_acl(
|
||||||
.global_ctx
|
data,
|
||||||
.get_acl_filter()
|
false,
|
||||||
.process_packet_with_acl(data, false, None, None)
|
None,
|
||||||
{
|
None,
|
||||||
|
&self.get_route(),
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{
|
||||||
|
HashMap, {BTreeMap, BTreeSet},
|
||||||
|
},
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
net::{Ipv4Addr, Ipv6Addr},
|
net::{Ipv4Addr, Ipv6Addr},
|
||||||
sync::{
|
sync::{
|
||||||
@@ -33,6 +35,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
peers::route_trait::{Route, RouteInterfaceBox},
|
peers::route_trait::{Route, RouteInterfaceBox},
|
||||||
proto::{
|
proto::{
|
||||||
|
acl::GroupIdentity,
|
||||||
common::{Ipv4Inet, NatType, StunInfo},
|
common::{Ipv4Inet, NatType, StunInfo},
|
||||||
peer_rpc::{
|
peer_rpc::{
|
||||||
route_foreign_network_infos, route_foreign_network_summary,
|
route_foreign_network_infos, route_foreign_network_summary,
|
||||||
@@ -127,6 +130,7 @@ impl RoutePeerInfo {
|
|||||||
network_length: 24,
|
network_length: 24,
|
||||||
quic_port: None,
|
quic_port: None,
|
||||||
ipv6_addr: None,
|
ipv6_addr: None,
|
||||||
|
groups: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +172,8 @@ impl RoutePeerInfo {
|
|||||||
|
|
||||||
quic_port: global_ctx.get_quic_proxy_port().map(|x| x as u32),
|
quic_port: global_ctx.get_quic_proxy_port().map(|x| x as u32),
|
||||||
ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()),
|
ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()),
|
||||||
|
|
||||||
|
groups: global_ctx.get_acl_groups(my_peer_id),
|
||||||
};
|
};
|
||||||
|
|
||||||
let need_update_periodically = if let Ok(Ok(d)) =
|
let need_update_periodically = if let Ok(Ok(d)) =
|
||||||
@@ -296,6 +302,8 @@ struct SyncedRouteInfo {
|
|||||||
raw_peer_infos: DashMap<PeerId, DynamicMessage>,
|
raw_peer_infos: DashMap<PeerId, DynamicMessage>,
|
||||||
conn_map: DashMap<PeerId, (BTreeSet<PeerId>, AtomicVersion)>,
|
conn_map: DashMap<PeerId, (BTreeSet<PeerId>, AtomicVersion)>,
|
||||||
foreign_network: DashMap<ForeignNetworkRouteInfoKey, ForeignNetworkRouteInfoEntry>,
|
foreign_network: DashMap<ForeignNetworkRouteInfoKey, ForeignNetworkRouteInfoEntry>,
|
||||||
|
group_trust_map: DashMap<PeerId, HashMap<String, Vec<u8>>>,
|
||||||
|
group_trust_map_cache: DashMap<PeerId, Arc<Vec<String>>>, // cache for group trust map, should sync with group_trust_map
|
||||||
|
|
||||||
version: AtomicVersion,
|
version: AtomicVersion,
|
||||||
}
|
}
|
||||||
@@ -306,6 +314,7 @@ impl Debug for SyncedRouteInfo {
|
|||||||
.field("peer_infos", &self.peer_infos)
|
.field("peer_infos", &self.peer_infos)
|
||||||
.field("conn_map", &self.conn_map)
|
.field("conn_map", &self.conn_map)
|
||||||
.field("foreign_network", &self.foreign_network)
|
.field("foreign_network", &self.foreign_network)
|
||||||
|
.field("group_trust_map", &self.group_trust_map)
|
||||||
.field("version", &self.version.get())
|
.field("version", &self.version.get())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
@@ -324,6 +333,8 @@ impl SyncedRouteInfo {
|
|||||||
self.raw_peer_infos.remove(&peer_id);
|
self.raw_peer_infos.remove(&peer_id);
|
||||||
self.conn_map.remove(&peer_id);
|
self.conn_map.remove(&peer_id);
|
||||||
self.foreign_network.retain(|k, _| k.peer_id != peer_id);
|
self.foreign_network.retain(|k, _| k.peer_id != peer_id);
|
||||||
|
self.group_trust_map.remove(&peer_id);
|
||||||
|
self.group_trust_map_cache.remove(&peer_id);
|
||||||
self.version.inc();
|
self.version.inc();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,6 +624,85 @@ impl SyncedRouteInfo {
|
|||||||
self.is_peer_bidirectly_connected(src_peer_id, dst_peer_id)
|
self.is_peer_bidirectly_connected(src_peer_id, dst_peer_id)
|
||||||
|| self.is_peer_bidirectly_connected(dst_peer_id, src_peer_id)
|
|| self.is_peer_bidirectly_connected(dst_peer_id, src_peer_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn verify_and_update_group_trusts(
|
||||||
|
&self,
|
||||||
|
peer_infos: &[RoutePeerInfo],
|
||||||
|
local_group_declarations: &[GroupIdentity],
|
||||||
|
) {
|
||||||
|
let local_group_declarations = local_group_declarations
|
||||||
|
.iter()
|
||||||
|
.map(|g| (g.group_name.as_str(), g.group_secret.as_str()))
|
||||||
|
.collect::<std::collections::HashMap<&str, &str>>();
|
||||||
|
|
||||||
|
let verify_groups = |old_trusted_groups: Option<&HashMap<String, Vec<u8>>>,
|
||||||
|
info: &RoutePeerInfo|
|
||||||
|
-> HashMap<String, Vec<u8>> {
|
||||||
|
let mut trusted_groups_for_peer: HashMap<String, Vec<u8>> = HashMap::new();
|
||||||
|
|
||||||
|
for group_proof in &info.groups {
|
||||||
|
let name = &group_proof.group_name;
|
||||||
|
let proof_bytes = group_proof.group_proof.clone();
|
||||||
|
|
||||||
|
// If we already trusted this group and the proof hasn't changed, reuse it.
|
||||||
|
if old_trusted_groups
|
||||||
|
.and_then(|g| g.get(name))
|
||||||
|
.map(|old| old == &proof_bytes)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
trusted_groups_for_peer.insert(name.clone(), proof_bytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(&local_secret) =
|
||||||
|
local_group_declarations.get(group_proof.group_name.as_str())
|
||||||
|
{
|
||||||
|
if group_proof.verify(local_secret, info.peer_id) {
|
||||||
|
trusted_groups_for_peer.insert(name.clone(), proof_bytes);
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
peer_id = info.peer_id,
|
||||||
|
group = %group_proof.group_name,
|
||||||
|
"Group proof verification failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trusted_groups_for_peer
|
||||||
|
};
|
||||||
|
|
||||||
|
for info in peer_infos {
|
||||||
|
match self.group_trust_map.entry(info.peer_id) {
|
||||||
|
dashmap::mapref::entry::Entry::Occupied(mut entry) => {
|
||||||
|
let old_trusted_groups = entry.get().clone();
|
||||||
|
let trusted_groups_for_peer = verify_groups(Some(&old_trusted_groups), info);
|
||||||
|
|
||||||
|
if trusted_groups_for_peer.is_empty() {
|
||||||
|
entry.remove();
|
||||||
|
self.group_trust_map_cache.remove(&info.peer_id);
|
||||||
|
} else {
|
||||||
|
self.group_trust_map_cache.insert(
|
||||||
|
info.peer_id,
|
||||||
|
Arc::new(trusted_groups_for_peer.keys().cloned().collect()),
|
||||||
|
);
|
||||||
|
*entry.get_mut() = trusted_groups_for_peer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dashmap::mapref::entry::Entry::Vacant(entry) => {
|
||||||
|
let trusted_groups_for_peer = verify_groups(None, info);
|
||||||
|
|
||||||
|
if !trusted_groups_for_peer.is_empty() {
|
||||||
|
self.group_trust_map_cache.insert(
|
||||||
|
info.peer_id,
|
||||||
|
Arc::new(trusted_groups_for_peer.keys().cloned().collect()),
|
||||||
|
);
|
||||||
|
entry.insert(trusted_groups_for_peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerGraph = Graph<PeerId, usize, Directed>;
|
type PeerGraph = Graph<PeerId, usize, Directed>;
|
||||||
@@ -1154,6 +1244,8 @@ impl PeerRouteServiceImpl {
|
|||||||
raw_peer_infos: DashMap::new(),
|
raw_peer_infos: DashMap::new(),
|
||||||
conn_map: DashMap::new(),
|
conn_map: DashMap::new(),
|
||||||
foreign_network: DashMap::new(),
|
foreign_network: DashMap::new(),
|
||||||
|
group_trust_map: DashMap::new(),
|
||||||
|
group_trust_map_cache: DashMap::new(),
|
||||||
version: AtomicVersion::new(),
|
version: AtomicVersion::new(),
|
||||||
},
|
},
|
||||||
cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::new()),
|
cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::new()),
|
||||||
@@ -1679,6 +1771,14 @@ impl PeerRouteServiceImpl {
|
|||||||
fn get_peer_info_last_update(&self) -> std::time::Instant {
|
fn get_peer_info_last_update(&self) -> std::time::Instant {
|
||||||
self.peer_info_last_update.load()
|
self.peer_info_last_update.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_peer_groups(&self, peer_id: PeerId) -> Arc<Vec<String>> {
|
||||||
|
self.synced_route_info
|
||||||
|
.group_trust_map_cache
|
||||||
|
.get(&peer_id)
|
||||||
|
.map(|groups| groups.value().clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for PeerRouteServiceImpl {
|
impl Drop for PeerRouteServiceImpl {
|
||||||
@@ -2016,6 +2116,12 @@ impl RouteSessionManager {
|
|||||||
peer_infos,
|
peer_infos,
|
||||||
raw_peer_infos.as_ref().unwrap(),
|
raw_peer_infos.as_ref().unwrap(),
|
||||||
)?;
|
)?;
|
||||||
|
service_impl
|
||||||
|
.synced_route_info
|
||||||
|
.verify_and_update_group_trusts(
|
||||||
|
peer_infos,
|
||||||
|
&service_impl.global_ctx.get_acl_group_declarations(),
|
||||||
|
);
|
||||||
session.update_dst_saved_peer_info_version(peer_infos);
|
session.update_dst_saved_peer_info_version(peer_infos);
|
||||||
need_update_route_table = true;
|
need_update_route_table = true;
|
||||||
}
|
}
|
||||||
@@ -2364,6 +2470,10 @@ impl Route for PeerRoute {
|
|||||||
async fn get_peer_info_last_update_time(&self) -> Instant {
|
async fn get_peer_info_last_update_time(&self) -> Instant {
|
||||||
self.service_impl.get_peer_info_last_update()
|
self.service_impl.get_peer_info_last_update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_peer_groups(&self, peer_id: PeerId) -> Arc<Vec<String>> {
|
||||||
|
self.service_impl.get_peer_groups(peer_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PeerPacketFilter for Arc<PeerRoute> {}
|
impl PeerPacketFilter for Arc<PeerRoute> {}
|
||||||
|
@@ -122,9 +122,58 @@ pub trait Route {
|
|||||||
|
|
||||||
async fn get_peer_info_last_update_time(&self) -> std::time::Instant;
|
async fn get_peer_info_last_update_time(&self) -> std::time::Instant;
|
||||||
|
|
||||||
|
fn get_peer_groups(&self, peer_id: PeerId) -> Arc<Vec<String>>;
|
||||||
|
|
||||||
|
async fn get_peer_groups_by_ip(&self, ip: &std::net::IpAddr) -> Arc<Vec<String>> {
|
||||||
|
match self.get_peer_id_by_ip(ip).await {
|
||||||
|
Some(peer_id) => self.get_peer_groups(peer_id),
|
||||||
|
None => Arc::new(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_peer_groups_by_ipv4(&self, ipv4: &Ipv4Addr) -> Arc<Vec<String>> {
|
||||||
|
match self.get_peer_id_by_ipv4(ipv4).await {
|
||||||
|
Some(peer_id) => self.get_peer_groups(peer_id),
|
||||||
|
None => Arc::new(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn dump(&self) -> String {
|
async fn dump(&self) -> String {
|
||||||
"this route implementation does not support dump".to_string()
|
"this route implementation does not support dump".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ArcRoute = Arc<Box<dyn Route + Send + Sync>>;
|
pub type ArcRoute = Arc<Box<dyn Route + Send + Sync>>;
|
||||||
|
|
||||||
|
pub struct MockRoute {}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Route for MockRoute {
|
||||||
|
async fn open(&self, _interface: RouteInterfaceBox) -> Result<u8, ()> {
|
||||||
|
panic!("mock route")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) {
|
||||||
|
panic!("mock route")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_next_hop(&self, _peer_id: PeerId) -> Option<PeerId> {
|
||||||
|
panic!("mock route")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_routes(&self) -> Vec<crate::proto::cli::Route> {
|
||||||
|
panic!("mock route")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_peer_info(&self, _peer_id: PeerId) -> Option<RoutePeerInfo> {
|
||||||
|
panic!("mock route")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_peer_info_last_update_time(&self) -> std::time::Instant {
|
||||||
|
panic!("mock route")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_peer_groups(&self, _peer_id: PeerId) -> Arc<Vec<String>> {
|
||||||
|
panic!("mock route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -67,6 +67,10 @@ message Rule {
|
|||||||
|
|
||||||
// Connection tracking
|
// Connection tracking
|
||||||
bool stateful = 13; // Enable connection tracking
|
bool stateful = 13; // Enable connection tracking
|
||||||
|
|
||||||
|
// Group matching criteria
|
||||||
|
repeated string source_groups = 14;
|
||||||
|
repeated string destination_groups = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule chain with metadata and optimization hints
|
// Rule chain with metadata and optimization hints
|
||||||
@@ -84,7 +88,20 @@ message Chain {
|
|||||||
Action default_action = 6;
|
Action default_action = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AclV1 { repeated Chain chains = 1; }
|
message GroupInfo {
|
||||||
|
repeated GroupIdentity declares = 1;
|
||||||
|
repeated string members = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupIdentity {
|
||||||
|
string group_name = 1;
|
||||||
|
string group_secret = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AclV1 {
|
||||||
|
repeated Chain chains = 1;
|
||||||
|
GroupInfo group = 2;
|
||||||
|
}
|
||||||
|
|
||||||
enum ConnState {
|
enum ConnState {
|
||||||
New = 0;
|
New = 0;
|
||||||
|
@@ -25,6 +25,8 @@ message RoutePeerInfo {
|
|||||||
|
|
||||||
optional uint32 quic_port = 14;
|
optional uint32 quic_port = 14;
|
||||||
optional common.Ipv6Inet ipv6_addr = 15;
|
optional common.Ipv6Inet ipv6_addr = 15;
|
||||||
|
|
||||||
|
repeated PeerGroupInfo groups = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PeerIdVersion {
|
message PeerIdVersion {
|
||||||
@@ -70,6 +72,11 @@ message RouteForeignNetworkSummary {
|
|||||||
map<uint32, Info> info_map = 1;
|
map<uint32, Info> info_map = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message PeerGroupInfo {
|
||||||
|
string group_name = 1;
|
||||||
|
bytes group_proof = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message SyncRouteInfoRequest {
|
message SyncRouteInfoRequest {
|
||||||
uint32 my_peer_id = 1;
|
uint32 my_peer_id = 1;
|
||||||
uint64 my_session_id = 2;
|
uint64 my_session_id = 2;
|
||||||
|
@@ -1 +1,245 @@
|
|||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::common::PeerId;
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/peer_rpc.rs"));
|
include!(concat!(env!("OUT_DIR"), "/peer_rpc.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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.clone(), 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1818,3 +1818,244 @@ pub async fn acl_rule_test_subnet_proxy(
|
|||||||
|
|
||||||
drop_insts(insts).await;
|
drop_insts(insts).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest::rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
pub async fn acl_group_based_test(
|
||||||
|
#[values("tcp", "udp")] protocol: &str,
|
||||||
|
#[values(true, false)] enable_kcp_proxy: bool,
|
||||||
|
#[values(true, false)] enable_quic_proxy: bool,
|
||||||
|
) {
|
||||||
|
use crate::tunnel::{
|
||||||
|
common::tests::_tunnel_pingpong_netns_with_timeout,
|
||||||
|
tcp::{TcpTunnelConnector, TcpTunnelListener},
|
||||||
|
udp::{UdpTunnelConnector, UdpTunnelListener},
|
||||||
|
TunnelConnector, TunnelListener,
|
||||||
|
};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
// 构造 ACL 配置,包含组信息
|
||||||
|
use crate::proto::acl::*;
|
||||||
|
|
||||||
|
// 设置组信息
|
||||||
|
let group_declares = vec![
|
||||||
|
GroupIdentity {
|
||||||
|
group_name: "admin".to_string(),
|
||||||
|
group_secret: "admin-secret".to_string(),
|
||||||
|
},
|
||||||
|
GroupIdentity {
|
||||||
|
group_name: "user".to_string(),
|
||||||
|
group_secret: "user-secret".to_string(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut chain = Chain {
|
||||||
|
name: "group_acl_test".to_string(),
|
||||||
|
chain_type: ChainType::Inbound as i32,
|
||||||
|
enabled: true,
|
||||||
|
default_action: Action::Drop as i32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 规则1: 允许admin组访问所有端口
|
||||||
|
let admin_allow_rule = Rule {
|
||||||
|
name: "allow_admin_all".to_string(),
|
||||||
|
priority: 300,
|
||||||
|
enabled: true,
|
||||||
|
action: Action::Allow as i32,
|
||||||
|
protocol: Protocol::Any as i32,
|
||||||
|
source_groups: vec!["admin".to_string()],
|
||||||
|
stateful: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
chain.rules.push(admin_allow_rule);
|
||||||
|
|
||||||
|
// 规则2: 允许user组访问8080端口
|
||||||
|
let user_8080_rule = Rule {
|
||||||
|
name: "allow_user_8080".to_string(),
|
||||||
|
priority: 200,
|
||||||
|
enabled: true,
|
||||||
|
action: Action::Allow as i32,
|
||||||
|
protocol: Protocol::Any as i32,
|
||||||
|
source_groups: vec!["user".to_string()],
|
||||||
|
ports: vec!["8080".to_string()],
|
||||||
|
stateful: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
chain.rules.push(user_8080_rule);
|
||||||
|
|
||||||
|
let acl_admin = Acl {
|
||||||
|
acl_v1: Some(AclV1 {
|
||||||
|
group: Some(GroupInfo {
|
||||||
|
declares: group_declares.clone(),
|
||||||
|
members: vec!["admin".to_string()],
|
||||||
|
}),
|
||||||
|
..AclV1::default()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let acl_user = Acl {
|
||||||
|
acl_v1: Some(AclV1 {
|
||||||
|
group: Some(GroupInfo {
|
||||||
|
declares: group_declares.clone(),
|
||||||
|
members: vec!["user".to_string()],
|
||||||
|
}),
|
||||||
|
..AclV1::default()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let acl_target = Acl {
|
||||||
|
acl_v1: Some(AclV1 {
|
||||||
|
chains: vec![chain.clone()],
|
||||||
|
group: Some(GroupInfo {
|
||||||
|
declares: group_declares.clone(),
|
||||||
|
members: vec![],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let insts = init_three_node_ex(
|
||||||
|
protocol,
|
||||||
|
move |cfg| {
|
||||||
|
match cfg.get_inst_name().as_str() {
|
||||||
|
"inst1" => {
|
||||||
|
cfg.set_acl(Some(acl_admin.clone()));
|
||||||
|
}
|
||||||
|
"inst2" => {
|
||||||
|
cfg.set_acl(Some(acl_user.clone()));
|
||||||
|
}
|
||||||
|
"inst3" => {
|
||||||
|
cfg.set_acl(Some(acl_target.clone()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut flags = cfg.get_flags();
|
||||||
|
flags.enable_kcp_proxy = enable_kcp_proxy;
|
||||||
|
flags.enable_quic_proxy = enable_quic_proxy;
|
||||||
|
cfg.set_flags(flags);
|
||||||
|
|
||||||
|
cfg
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
println!("Testing group-based ACL rules...");
|
||||||
|
|
||||||
|
let make_listener = |port: u16| -> Box<dyn TunnelListener + Send + Sync + 'static> {
|
||||||
|
match protocol {
|
||||||
|
"tcp" => Box::new(TcpTunnelListener::new(
|
||||||
|
format!("tcp://0.0.0.0:{}", port).parse().unwrap(),
|
||||||
|
)),
|
||||||
|
"udp" => Box::new(UdpTunnelListener::new(
|
||||||
|
format!("udp://0.0.0.0:{}", port).parse().unwrap(),
|
||||||
|
)),
|
||||||
|
_ => panic!("unsupported protocol: {}", protocol),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let make_connector = |port: u16| -> Box<dyn TunnelConnector + Send + Sync + 'static> {
|
||||||
|
match protocol {
|
||||||
|
"tcp" => Box::new(TcpTunnelConnector::new(
|
||||||
|
format!("tcp://10.144.144.3:{}", port).parse().unwrap(),
|
||||||
|
)),
|
||||||
|
"udp" => Box::new(UdpTunnelConnector::new(
|
||||||
|
format!("udp://10.144.144.3:{}", port).parse().unwrap(),
|
||||||
|
)),
|
||||||
|
_ => panic!("unsupported protocol: {}", protocol),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 构造测试数据
|
||||||
|
let mut buf = vec![0; 32];
|
||||||
|
rand::thread_rng().fill(&mut buf[..]);
|
||||||
|
|
||||||
|
// 测试1: inst1 (admin组) 访问8080 - 应该成功
|
||||||
|
let result = _tunnel_pingpong_netns_with_timeout(
|
||||||
|
make_listener(8080),
|
||||||
|
make_connector(8080),
|
||||||
|
NetNS::new(Some("net_c".into())),
|
||||||
|
NetNS::new(Some("net_a".into())),
|
||||||
|
buf.clone(),
|
||||||
|
std::time::Duration::from_millis(30000),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Admin group access to port 8080 should be allowed (protocol={})",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"✓ Admin group access to port 8080 succeeded ({})\n",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试2: inst1 (admin组) 访问8081 - 应该成功
|
||||||
|
let result = _tunnel_pingpong_netns_with_timeout(
|
||||||
|
make_listener(8081),
|
||||||
|
make_connector(8081),
|
||||||
|
NetNS::new(Some("net_c".into())),
|
||||||
|
NetNS::new(Some("net_a".into())),
|
||||||
|
buf.clone(),
|
||||||
|
std::time::Duration::from_millis(30000),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"Admin group access to port 8081 should be allowed (protocol={})",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"✓ Admin group access to port 8081 succeeded ({})\n",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试3: inst2 (user组) 访问8080 - 应该成功
|
||||||
|
let result = _tunnel_pingpong_netns_with_timeout(
|
||||||
|
make_listener(8080),
|
||||||
|
make_connector(8080),
|
||||||
|
NetNS::new(Some("net_c".into())),
|
||||||
|
NetNS::new(Some("net_b".into())),
|
||||||
|
buf.clone(),
|
||||||
|
std::time::Duration::from_millis(30000),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"User group access to port 8080 should be allowed (protocol={})",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"✓ User group access to port 8080 succeeded ({})\n",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试4: inst2 (user组) 访问8081 - 应该失败
|
||||||
|
let result = _tunnel_pingpong_netns_with_timeout(
|
||||||
|
make_listener(8081),
|
||||||
|
make_connector(8081),
|
||||||
|
NetNS::new(Some("net_c".into())),
|
||||||
|
NetNS::new(Some("net_b".into())),
|
||||||
|
buf.clone(),
|
||||||
|
std::time::Duration::from_millis(200),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"User group access to port 8081 should be blocked (protocol={})",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"✓ User group access to port 8081 blocked as expected ({})\n",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
|
||||||
|
let stats = insts[2].get_global_ctx().get_acl_filter().get_stats();
|
||||||
|
println!("ACL stats after group {} tests: {:?}", protocol, stats);
|
||||||
|
|
||||||
|
println!("✓ All group-based ACL tests completed successfully");
|
||||||
|
|
||||||
|
drop_insts(insts).await;
|
||||||
|
}
|
||||||
|
@@ -560,6 +560,45 @@ pub mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn _tunnel_pingpong_netns_with_timeout<L, C>(
|
||||||
|
listener: L,
|
||||||
|
connector: C,
|
||||||
|
l_netns: NetNS,
|
||||||
|
c_netns: NetNS,
|
||||||
|
buf: Vec<u8>,
|
||||||
|
timeout: std::time::Duration,
|
||||||
|
) -> Result<(), anyhow::Error>
|
||||||
|
where
|
||||||
|
L: TunnelListener + Send + Sync + 'static,
|
||||||
|
C: TunnelConnector + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
_tunnel_pingpong_netns(listener, connector, l_netns, c_netns, buf).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
match tokio::time::timeout(timeout, handle).await {
|
||||||
|
Ok(join_res) => match join_res {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(join_err) => {
|
||||||
|
if join_err.is_panic() {
|
||||||
|
let payload = join_err.into_panic();
|
||||||
|
let msg = match payload.downcast::<String>() {
|
||||||
|
Ok(s) => *s,
|
||||||
|
Err(payload) => match payload.downcast::<&str>() {
|
||||||
|
Ok(s) => (*s).to_string(),
|
||||||
|
Err(_) => "non-string panic payload".to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Err(anyhow::anyhow!("task panicked: {}", msg))
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!("task cancelled"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(elapsed) => Err(elapsed.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn _tunnel_bench<L, C>(listener: L, connector: C)
|
pub(crate) async fn _tunnel_bench<L, C>(listener: L, connector: C)
|
||||||
where
|
where
|
||||||
L: TunnelListener + Send + Sync + 'static,
|
L: TunnelListener + Send + Sync + 'static,
|
||||||
|
@@ -679,6 +679,14 @@ impl ZCPacket {
|
|||||||
ZCPacketType::DummyTunnel,
|
ZCPacketType::DummyTunnel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_src_peer_id(&self) -> Option<u32> {
|
||||||
|
self.peer_manager_header().map(|hdr| hdr.from_peer_id.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dst_peer_id(&self) -> Option<u32> {
|
||||||
|
self.peer_manager_header().map(|hdr| hdr.to_peer_id.get())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
12
flake.lock
generated
12
flake.lock
generated
@@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753429684,
|
"lastModified": 1754725699,
|
||||||
"narHash": "sha256-9h7+4/53cSfQ/uA3pSvCaBepmZaz/dLlLVJnbQ+SJjk=",
|
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7fd36ee82c0275fb545775cc5e4d30542899511d",
|
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -48,11 +48,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1753671061,
|
"lastModified": 1754966322,
|
||||||
"narHash": "sha256-IU4eBWfe9h2QejJYST+EAlhg8a1H6mh9gbcmWgZ2/mQ=",
|
"narHash": "sha256-7f/LH60DnjjQVKbXAsHIniGaU7ixVM7eWU3hyjT24YI=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "40065d17ee4dbec3ded8ca61236132aede843fab",
|
"rev": "7c13cec2e3828d964b9980d0ffd680bd8d4dce90",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
Reference in New Issue
Block a user