Implement ACL (#1140)

1. get acl stats
```
./easytier-cli acl stats
AclStats:
  Global:
    CacheHits: 4
    CacheMaxSize: 10000
    CacheSize: 5
    DefaultAllows: 3
    InboundPacketsAllowed: 2
    InboundPacketsTotal: 2
    OutboundPacketsAllowed: 7
    OutboundPacketsTotal: 7
    PacketsAllowed: 9
    PacketsTotal: 9
    RuleMatches: 2
  ConnTrack:
    [src: 10.14.11.1:57444, dst: 10.14.11.2:1000, proto: Tcp, state: New, pkts: 1, bytes: 60, created: 2025-07-24 10:13:39 +08:00, last_seen: 2025-07-24 10:13:39 +08:00]
  Rules:
    [name: 'tcp_whitelist', prio: 1000, action: Allow, enabled: true, proto: Tcp, ports: ["1000"], src_ports: [], src_ips: [], dst_ips: [], stateful: true, rate: 0, burst: 0] [pkts: 2, bytes: 120]

  ```
2. use tcp/udp whitelist to block unexpected traffic.
   `sudo ./easytier-core -d --tcp-whitelist 1000`

3. use complete acl ability with config file:

```
[[acl.acl_v1.chains]]
name = "inbound_whitelist"
chain_type = 1
description = "Auto-generated inbound whitelist from CLI"
enabled = true
default_action = 2

[[acl.acl_v1.chains.rules]]
name = "tcp_whitelist"
description = "Auto-generated TCP whitelist rule"
priority = 1000
enabled = true
protocol = 1
ports = ["1000"]
source_ips = []
destination_ips = []
source_ports = []
action = 1
rate_limit = 0
burst_limit = 0
stateful = true

```
This commit is contained in:
Sijie.Sun
2025-07-24 22:13:45 +08:00
committed by GitHub
parent 4f53fccd25
commit 8e7a8de5e5
22 changed files with 2377 additions and 28 deletions

1
Cargo.lock generated
View File

@@ -1955,6 +1955,7 @@ version = "2.3.2"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
"arc-swap",
"async-recursion", "async-recursion",
"async-ringbuf", "async-ringbuf",
"async-stream", "async-stream",

View File

@@ -40,6 +40,7 @@ tracing-appender = "0.2.3"
thiserror = "1.0" thiserror = "1.0"
auto_impl = "1.1.0" auto_impl = "1.1.0"
crossbeam = "0.8.4" crossbeam = "0.8.4"
arc-swap = "1.7"
time = "0.3" time = "0.3"
toml = "0.8.12" toml = "0.8.12"
chrono = { version = "0.4.37", features = ["serde"] } chrono = { version = "0.4.37", features = ["serde"] }

View File

@@ -147,6 +147,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"src/proto/cli.proto", "src/proto/cli.proto",
"src/proto/web.proto", "src/proto/web.proto",
"src/proto/magic_dns.proto", "src/proto/magic_dns.proto",
"src/proto/acl.proto",
]; ];
for proto_file in proto_files.iter().chain(proto_files_reflect.iter()) { for proto_file in proto_files.iter().chain(proto_files_reflect.iter()) {
@@ -156,6 +157,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = prost_build::Config::new(); let mut config = prost_build::Config::new();
config config
.protoc_arg("--experimental_allow_proto3_optional") .protoc_arg("--experimental_allow_proto3_optional")
.type_attribute(".acl", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]") .type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]") .type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".cli", "#[derive(serde::Serialize, serde::Deserialize)]") .type_attribute(".cli", "#[derive(serde::Serialize, serde::Deserialize)]")

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,10 @@ use cidr::IpCidr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
proto::common::{CompressionAlgoPb, PortForwardConfigPb, SocketType}, proto::{
acl::Acl,
common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
},
tunnel::generate_digest_from_str, tunnel::generate_digest_from_str,
}; };
@@ -116,6 +119,9 @@ pub trait ConfigLoader: Send + Sync {
fn get_port_forwards(&self) -> Vec<PortForwardConfig>; fn get_port_forwards(&self) -> Vec<PortForwardConfig>;
fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>); fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>);
fn get_acl(&self) -> Option<Acl>;
fn set_acl(&self, acl: Option<Acl>);
fn dump(&self) -> String; fn dump(&self) -> String;
} }
@@ -291,6 +297,8 @@ struct Config {
#[serde(skip)] #[serde(skip)]
flags_struct: Option<Flags>, flags_struct: Option<Flags>,
acl: Option<Acl>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -649,6 +657,14 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().port_forward = Some(forwards); self.config.lock().unwrap().port_forward = Some(forwards);
} }
fn get_acl(&self) -> Option<Acl> {
self.config.lock().unwrap().acl.clone()
}
fn set_acl(&self, acl: Option<Acl>) {
self.config.lock().unwrap().acl = acl;
}
fn dump(&self) -> String { fn dump(&self) -> String {
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap(); let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
let default_flags_hashmap = let default_flags_hashmap =

View File

@@ -6,6 +6,7 @@ use std::{
use crate::common::config::ProxyNetworkConfig; use crate::common::config::ProxyNetworkConfig;
use crate::common::token_bucket::TokenBucketManager; use crate::common::token_bucket::TokenBucketManager;
use crate::peers::acl_filter::AclFilter;
use crate::proto::cli::PeerConnInfo; use crate::proto::cli::PeerConnInfo;
use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb}; use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb};
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
@@ -81,6 +82,8 @@ pub struct GlobalCtx {
quic_proxy_port: AtomicCell<Option<u16>>, quic_proxy_port: AtomicCell<Option<u16>>,
token_bucket_manager: TokenBucketManager, token_bucket_manager: TokenBucketManager,
acl_filter: Arc<AclFilter>,
} }
impl std::fmt::Debug for GlobalCtx { impl std::fmt::Debug for GlobalCtx {
@@ -108,7 +111,7 @@ impl GlobalCtx {
let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers()); let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers());
let enable_exit_node = config_fs.get_flags().enable_exit_node || cfg!(target_env= "ohos"); let enable_exit_node = config_fs.get_flags().enable_exit_node || cfg!(target_env = "ohos");
let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system; let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system;
let no_tun = config_fs.get_flags().no_tun; let no_tun = config_fs.get_flags().no_tun;
@@ -147,6 +150,8 @@ impl GlobalCtx {
quic_proxy_port: AtomicCell::new(None), quic_proxy_port: AtomicCell::new(None),
token_bucket_manager: TokenBucketManager::new(), token_bucket_manager: TokenBucketManager::new(),
acl_filter: Arc::new(AclFilter::new()),
} }
} }
@@ -317,6 +322,10 @@ impl GlobalCtx {
pub fn token_bucket_manager(&self) -> &TokenBucketManager { pub fn token_bucket_manager(&self) -> &TokenBucketManager {
&self.token_bucket_manager &self.token_bucket_manager
} }
pub fn get_acl_filter(&self) -> &Arc<AclFilter> {
&self.acl_filter
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -10,6 +10,7 @@ use tracing::Instrument;
use crate::{set_global_var, use_global_var}; use crate::{set_global_var, use_global_var};
pub mod acl_processor;
pub mod compressor; pub mod compressor;
pub mod config; pub mod config;
pub mod constants; pub mod constants;

View File

@@ -27,11 +27,12 @@ use easytier::{
}, },
proto::{ proto::{
cli::{ cli::{
list_peer_route_pair, ConnectorManageRpc, ConnectorManageRpcClientFactory, list_peer_route_pair, AclManageRpc, AclManageRpcClientFactory, ConnectorManageRpc,
DumpRouteRequest, GetVpnPortalInfoRequest, ListConnectorRequest, ConnectorManageRpcClientFactory, DumpRouteRequest, GetAclStatsRequest,
ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest, GetVpnPortalInfoRequest, ListConnectorRequest, ListForeignNetworkRequest,
ListPeerRequest, ListPeerResponse, ListRouteRequest, ListRouteResponse, ListGlobalForeignNetworkRequest, ListMappedListenerRequest, ListPeerRequest,
ManageMappedListenerRequest, MappedListenerManageAction, MappedListenerManageRpc, ListPeerResponse, ListRouteRequest, ListRouteResponse, ManageMappedListenerRequest,
MappedListenerManageAction, MappedListenerManageRpc,
MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc, MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc,
PeerManageRpcClientFactory, ShowNodeInfoRequest, TcpProxyEntryState, PeerManageRpcClientFactory, ShowNodeInfoRequest, TcpProxyEntryState,
TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc, TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc,
@@ -93,6 +94,8 @@ enum SubCommand {
Service(ServiceArgs), Service(ServiceArgs),
#[command(about = "show tcp/kcp proxy status")] #[command(about = "show tcp/kcp proxy status")]
Proxy, Proxy,
#[command(about = "show ACL rules statistics")]
Acl(AclArgs),
#[command(about = t!("core_clap.generate_completions").to_string())] #[command(about = t!("core_clap.generate_completions").to_string())]
GenAutocomplete { shell: Shell }, GenAutocomplete { shell: Shell },
} }
@@ -179,6 +182,17 @@ struct NodeArgs {
sub_command: Option<NodeSubCommand>, sub_command: Option<NodeSubCommand>,
} }
#[derive(Args, Debug)]
struct AclArgs {
#[command(subcommand)]
sub_command: Option<AclSubCommand>,
}
#[derive(Subcommand, Debug)]
enum AclSubCommand {
Stats,
}
#[derive(Args, Debug)] #[derive(Args, Debug)]
struct ServiceArgs { struct ServiceArgs {
#[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")] #[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")]
@@ -301,6 +315,18 @@ impl CommandHandler<'_> {
.with_context(|| "failed to get vpn portal client")?) .with_context(|| "failed to get vpn portal client")?)
} }
async fn get_acl_manager_client(
&self,
) -> Result<Box<dyn AclManageRpc<Controller = BaseController>>, Error> {
Ok(self
.client
.lock()
.unwrap()
.scoped_client::<AclManageRpcClientFactory<BaseController>>("".to_string())
.await
.with_context(|| "failed to get acl manager client")?)
}
async fn get_tcp_proxy_client( async fn get_tcp_proxy_client(
&self, &self,
transport_type: &str, transport_type: &str,
@@ -688,6 +714,26 @@ impl CommandHandler<'_> {
Ok(()) Ok(())
} }
async fn handle_acl_stats(&self) -> Result<(), Error> {
let client = self.get_acl_manager_client().await?;
let request = GetAclStatsRequest::default();
let response = client
.get_acl_stats(BaseController::default(), request)
.await?;
if let Some(acl_stats) = response.acl_stats {
if self.output_format == &OutputFormat::Json {
println!("{}", serde_json::to_string_pretty(&acl_stats)?);
} else {
println!("{}", acl_stats);
}
} else {
println!("No ACL statistics available");
}
Ok(())
}
async fn handle_mapped_listener_list(&self) -> Result<(), Error> { async fn handle_mapped_listener_list(&self) -> Result<(), Error> {
let client = self.get_mapped_listener_manager_client().await?; let client = self.get_mapped_listener_manager_client().await?;
let request = ListMappedListenerRequest::default(); let request = ListMappedListenerRequest::default();
@@ -1443,6 +1489,11 @@ async fn main() -> Result<(), Error> {
print_output(&table_rows, &cli.output_format)?; print_output(&table_rows, &cli.output_format)?;
} }
SubCommand::Acl(acl_args) => match &acl_args.sub_command {
Some(AclSubCommand::Stats) | None => {
handler.handle_acl_stats().await?;
}
},
SubCommand::GenAutocomplete { shell } => { SubCommand::GenAutocomplete { shell } => {
let mut cmd = Cli::command(); let mut cmd = Cli::command();
easytier::print_completions(shell, &mut cmd, "easytier-cli"); easytier::print_completions(shell, &mut cmd, "easytier-cli");

View File

@@ -29,7 +29,10 @@ use easytier::{
connector::create_connector_by_url, connector::create_connector_by_url,
instance_manager::NetworkInstanceManager, instance_manager::NetworkInstanceManager,
launcher::{add_proxy_network_to_config, ConfigSource}, launcher::{add_proxy_network_to_config, ConfigSource},
proto::common::{CompressionAlgoPb, NatType}, proto::{
acl::{Acl, AclV1, Action, Chain, ChainType, Protocol, Rule},
common::{CompressionAlgoPb, NatType},
},
tunnel::{IpVersion, PROTO_PORT_OFFSET}, tunnel::{IpVersion, PROTO_PORT_OFFSET},
utils::{init_logger, setup_panic_handler}, utils::{init_logger, setup_panic_handler},
web_client, web_client,
@@ -506,6 +509,22 @@ struct NetworkOptions {
help = t!("core_clap.foreign_relay_bps_limit").to_string(), help = t!("core_clap.foreign_relay_bps_limit").to_string(),
)] )]
foreign_relay_bps_limit: Option<u64>, foreign_relay_bps_limit: Option<u64>,
#[arg(
long,
value_delimiter = ',',
help = "TCP port whitelist. Supports single ports (80) and ranges (8000-9000)",
num_args = 0..
)]
tcp_whitelist: Vec<String>,
#[arg(
long,
value_delimiter = ',',
help = "UDP port whitelist. Supports single ports (53) and ranges (5000-6000)",
num_args = 0..
)]
udp_whitelist: Vec<String>,
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -603,6 +622,117 @@ impl NetworkOptions {
false false
} }
fn parse_port_list(port_list: &[String]) -> anyhow::Result<Vec<String>> {
let mut ports = Vec::new();
for port_spec in port_list {
if port_spec.contains('-') {
// Handle port range like "8000-9000"
let parts: Vec<&str> = port_spec.split('-').collect();
if parts.len() != 2 {
return Err(anyhow::anyhow!("Invalid port range format: {}", port_spec));
}
let start: u16 = parts[0]
.parse()
.with_context(|| format!("Invalid start port in range: {}", port_spec))?;
let end: u16 = parts[1]
.parse()
.with_context(|| format!("Invalid end port in range: {}", port_spec))?;
if start > end {
return Err(anyhow::anyhow!(
"Start port must be <= end port in range: {}",
port_spec
));
}
// Add individual ports in the range
for port in start..=end {
ports.push(port.to_string());
}
} else {
// Handle single port
let port: u16 = port_spec
.parse()
.with_context(|| format!("Invalid port number: {}", port_spec))?;
ports.push(port.to_string());
}
}
Ok(ports)
}
fn generate_acl_from_whitelists(&self) -> anyhow::Result<Option<Acl>> {
if self.tcp_whitelist.is_empty() && self.udp_whitelist.is_empty() {
return Ok(None);
}
let mut acl = Acl {
acl_v1: Some(AclV1 { chains: vec![] }),
};
let acl_v1 = acl.acl_v1.as_mut().unwrap();
// Create inbound chain for whitelist rules
let mut inbound_chain = Chain {
name: "inbound_whitelist".to_string(),
chain_type: ChainType::Inbound as i32,
description: "Auto-generated inbound whitelist from CLI".to_string(),
enabled: true,
rules: vec![],
default_action: Action::Drop as i32, // Default deny
};
let mut rule_priority = 1000u32;
// Add TCP whitelist rules
if !self.tcp_whitelist.is_empty() {
let tcp_ports = Self::parse_port_list(&self.tcp_whitelist)?;
let tcp_rule = Rule {
name: "tcp_whitelist".to_string(),
description: "Auto-generated TCP whitelist rule".to_string(),
priority: rule_priority,
enabled: true,
protocol: Protocol::Tcp as i32,
ports: tcp_ports,
source_ips: vec![],
destination_ips: vec![],
source_ports: vec![],
action: Action::Allow as i32,
rate_limit: 0,
burst_limit: 0,
stateful: true,
};
inbound_chain.rules.push(tcp_rule);
rule_priority -= 1;
}
// Add UDP whitelist rules
if !self.udp_whitelist.is_empty() {
let udp_ports = Self::parse_port_list(&self.udp_whitelist)?;
let udp_rule = Rule {
name: "udp_whitelist".to_string(),
description: "Auto-generated UDP whitelist rule".to_string(),
priority: rule_priority,
enabled: true,
protocol: Protocol::Udp as i32,
ports: udp_ports,
source_ips: vec![],
destination_ips: vec![],
source_ports: vec![],
action: Action::Allow as i32,
rate_limit: 0,
burst_limit: 0,
stateful: false,
};
inbound_chain.rules.push(udp_rule);
}
acl_v1.chains.push(inbound_chain);
Ok(Some(acl))
}
fn merge_into(&self, cfg: &mut TomlConfigLoader) -> anyhow::Result<()> { fn merge_into(&self, cfg: &mut TomlConfigLoader) -> anyhow::Result<()> {
if self.hostname.is_some() { if self.hostname.is_some() {
cfg.set_hostname(self.hostname.clone()); cfg.set_hostname(self.hostname.clone());
@@ -860,6 +990,11 @@ impl NetworkOptions {
cfg.set_exit_nodes(self.exit_nodes.clone()); cfg.set_exit_nodes(self.exit_nodes.clone());
} }
// Handle port whitelists by generating ACL configuration
if let Some(acl) = self.generate_acl_from_whitelists()? {
cfg.set_acl(Some(acl));
}
Ok(()) Ok(())
} }
} }

View File

@@ -298,7 +298,10 @@ impl NicPacketFilter for MagicDnsServerInstanceData {
#[async_trait::async_trait] #[async_trait::async_trait]
impl RpcServerHook for MagicDnsServerInstanceData { impl RpcServerHook for MagicDnsServerInstanceData {
async fn on_new_client(&self, tunnel_info: Option<TunnelInfo>)-> Result<Option<TunnelInfo>, anyhow::Error> { async fn on_new_client(
&self,
tunnel_info: Option<TunnelInfo>,
) -> Result<Option<TunnelInfo>, anyhow::Error> {
tracing::info!(?tunnel_info, "New client connected"); tracing::info!(?tunnel_info, "New client connected");
Ok(tunnel_info) Ok(tunnel_info)
} }

View File

@@ -609,6 +609,10 @@ impl Instance {
} }
} }
if let Some(acl) = self.global_ctx.config.get_acl() {
self.global_ctx.get_acl_filter().reload_rules(Some(&acl));
}
// run after tun device created, so listener can bind to tun device, which may be required by win 10 // run after tun device created, so listener can bind to tun device, which may be required by win 10
self.ip_proxy = Some(IpProxy::new( self.ip_proxy = Some(IpProxy::new(
self.get_global_ctx(), self.get_global_ctx(),
@@ -801,10 +805,11 @@ impl Instance {
let mapped_listener_manager_rpc = self.get_mapped_listener_manager_rpc_service(); let mapped_listener_manager_rpc = self.get_mapped_listener_manager_rpc_service();
let s = self.rpc_server.as_mut().unwrap(); let s = self.rpc_server.as_mut().unwrap();
s.registry().register( let peer_mgr_rpc_service = PeerManagerRpcService::new(peer_mgr.clone());
PeerManageRpcServer::new(PeerManagerRpcService::new(peer_mgr)), s.registry()
"", .register(PeerManageRpcServer::new(peer_mgr_rpc_service.clone()), "");
); s.registry()
.register(AclManageRpcServer::new(peer_mgr_rpc_service), "");
s.registry().register( s.registry().register(
ConnectorManageRpcServer::new(ConnectorManagerRpcService(conn_manager)), ConnectorManageRpcServer::new(ConnectorManagerRpcService(conn_manager)),
"", "",

View File

@@ -0,0 +1,289 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::atomic::Ordering;
use std::{
net::IpAddr,
sync::{atomic::AtomicBool, Arc},
};
use arc_swap::ArcSwap;
use pnet::packet::ipv6::Ipv6Packet;
use pnet::packet::{
ip::IpNextHeaderProtocols, ipv4::Ipv4Packet, tcp::TcpPacket, udp::UdpPacket, Packet as _,
};
use crate::proto::acl::{AclStats, Protocol};
use crate::tunnel::packet_def::PacketType;
use crate::{
common::acl_processor::{AclProcessor, AclResult, AclStatKey, AclStatType, PacketInfo},
proto::acl::{Acl, Action, ChainType},
tunnel::packet_def::ZCPacket,
};
/// ACL filter that can be inserted into the packet processing pipeline
/// Optimized with lock-free hot reloading via atomic processor replacement
pub struct AclFilter {
// Use ArcSwap for lock-free atomic replacement during hot reload
acl_processor: ArcSwap<AclProcessor>,
acl_enabled: Arc<AtomicBool>,
}
impl AclFilter {
pub fn new() -> Self {
Self {
acl_processor: ArcSwap::from(Arc::new(AclProcessor::new(Acl::default()))),
acl_enabled: Arc::new(AtomicBool::new(false)),
}
}
/// Hot reload ACL rules by creating a new processor instance
/// Preserves connection tracking and rate limiting state across reloads
/// Now lock-free and doesn't require &mut self!
pub fn reload_rules(&self, acl_config: Option<&Acl>) {
let Some(acl_config) = acl_config else {
self.acl_enabled.store(false, Ordering::Relaxed);
return;
};
// Get current processor to extract shared state
let current_processor = self.acl_processor.load();
let (conn_track, rate_limiters, stats) = current_processor.get_shared_state();
// Create new processor with preserved state
let new_processor = AclProcessor::new_with_shared_state(
acl_config.clone(),
Some(conn_track),
Some(rate_limiters),
Some(stats),
);
// Atomic replacement - this is completely lock-free!
self.acl_processor.store(Arc::new(new_processor));
self.acl_enabled.store(true, Ordering::Relaxed);
tracing::info!("ACL rules hot reloaded with preserved state (lock-free)");
}
/// Get current processor for processing packets
fn get_processor(&self) -> Arc<AclProcessor> {
self.acl_processor.load_full()
}
pub fn get_stats(&self) -> AclStats {
let processor = self.get_processor();
let global_stats = processor.get_stats();
let (conn_track, _, _) = processor.get_shared_state();
let rules_stats = processor.get_rules_stats();
AclStats {
global: global_stats.into_iter().map(|(k, v)| (k, v)).collect(),
conn_track: conn_track.iter().map(|x| x.value().clone()).collect(),
rules: rules_stats,
}
}
/// Extract packet information for ACL processing
fn extract_packet_info(&self, packet: &ZCPacket) -> Option<PacketInfo> {
let payload = packet.payload();
let src_ip;
let dst_ip;
let src_port;
let dst_port;
let protocol;
let ipv4_packet = Ipv4Packet::new(payload)?;
if ipv4_packet.get_version() == 4 {
src_ip = IpAddr::V4(ipv4_packet.get_source());
dst_ip = IpAddr::V4(ipv4_packet.get_destination());
protocol = ipv4_packet.get_next_level_protocol();
(src_port, dst_port) = match protocol {
IpNextHeaderProtocols::Tcp => {
let tcp_packet = TcpPacket::new(ipv4_packet.payload())?;
(
Some(tcp_packet.get_source()),
Some(tcp_packet.get_destination()),
)
}
IpNextHeaderProtocols::Udp => {
let udp_packet = UdpPacket::new(ipv4_packet.payload())?;
(
Some(udp_packet.get_source()),
Some(udp_packet.get_destination()),
)
}
_ => (None, None),
};
} else if ipv4_packet.get_version() == 6 {
let ipv6_packet = Ipv6Packet::new(payload)?;
src_ip = IpAddr::V6(ipv6_packet.get_source());
dst_ip = IpAddr::V6(ipv6_packet.get_destination());
protocol = ipv6_packet.get_next_header();
(src_port, dst_port) = match protocol {
IpNextHeaderProtocols::Tcp => {
let tcp_packet = TcpPacket::new(ipv6_packet.payload())?;
(
Some(tcp_packet.get_source()),
Some(tcp_packet.get_destination()),
)
}
IpNextHeaderProtocols::Udp => {
let udp_packet = UdpPacket::new(ipv6_packet.payload())?;
(
Some(udp_packet.get_source()),
Some(udp_packet.get_destination()),
)
}
_ => (None, None),
};
} else {
return None;
}
let acl_protocol = match protocol {
IpNextHeaderProtocols::Tcp => Protocol::Tcp,
IpNextHeaderProtocols::Udp => Protocol::Udp,
IpNextHeaderProtocols::Icmp => Protocol::Icmp,
IpNextHeaderProtocols::Icmpv6 => Protocol::IcmPv6,
_ => Protocol::Unspecified,
};
Some(PacketInfo {
src_ip,
dst_ip,
src_port,
dst_port,
protocol: acl_protocol,
packet_size: payload.len(),
})
}
/// Process ACL result and log if needed
fn handle_acl_result(
&self,
result: &AclResult,
packet_info: &PacketInfo,
chain_type: ChainType,
processor: &AclProcessor,
) {
if result.should_log {
if let Some(ref log_context) = result.log_context {
let log_message = log_context.to_message();
tracing::info!(
src_ip = %packet_info.src_ip,
dst_ip = %packet_info.dst_ip,
src_port = packet_info.src_port,
dst_port = packet_info.dst_port,
protocol = ?packet_info.protocol,
action = ?result.action,
rule = result.matched_rule_str().as_deref().unwrap_or("unknown"),
chain_type = ?chain_type,
"ACL: {}", log_message
);
}
}
// Update global statistics in the ACL processor
match result.action {
Action::Allow => {
processor.increment_stat(AclStatKey::PacketsAllowed);
processor.increment_stat(AclStatKey::from_chain_and_action(
chain_type,
AclStatType::Allowed,
));
tracing::trace!("ACL: Packet allowed");
}
Action::Drop => {
processor.increment_stat(AclStatKey::PacketsDropped);
processor.increment_stat(AclStatKey::from_chain_and_action(
chain_type,
AclStatType::Dropped,
));
tracing::debug!("ACL: Packet dropped");
}
Action::Noop => {
processor.increment_stat(AclStatKey::PacketsNoop);
processor.increment_stat(AclStatKey::from_chain_and_action(
chain_type,
AclStatType::Noop,
));
tracing::trace!("ACL: No operation");
}
}
// Track total packets processed per chain
processor.increment_stat(AclStatKey::from_chain_and_action(
chain_type,
AclStatType::Total,
));
processor.increment_stat(AclStatKey::PacketsTotal);
}
/// Common ACL processing logic
pub fn process_packet_with_acl(
&self,
packet: &ZCPacket,
is_in: bool,
my_ipv4: Option<Ipv4Addr>,
my_ipv6: Option<Ipv6Addr>,
) -> bool {
if !self.acl_enabled.load(Ordering::Relaxed) {
return true;
}
if packet.peer_manager_header().unwrap().packet_type != PacketType::Data as u8 {
return true;
}
// Extract packet information
let packet_info = match self.extract_packet_info(packet) {
Some(info) => info,
None => {
tracing::warn!(
"Failed to extract packet info from {:?} packet, header: {:?}",
if is_in { "inbound" } else { "outbound" },
packet.peer_manager_header()
);
// allow all unknown packets
return true;
}
};
let chain_type = if is_in {
if packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|| packet_info.dst_ip == my_ipv6.unwrap_or(Ipv6Addr::UNSPECIFIED)
{
ChainType::Inbound
} else {
ChainType::Forward
}
} else {
ChainType::Outbound
};
// Get current processor atomically
let processor = self.get_processor();
// Process through ACL rules
let acl_result = processor.process_packet(&packet_info, chain_type);
self.handle_acl_result(&acl_result, &packet_info, chain_type, &processor);
// Check if packet should be allowed
match acl_result.action {
Action::Allow | Action::Noop => true,
Action::Drop => {
tracing::trace!(
"ACL: Dropping {:?} packet from {} to {}, chain_type: {:?}",
packet_info.protocol,
packet_info.src_ip,
packet_info.dst_ip,
chain_type,
);
false
}
}
}
}

View File

@@ -1,5 +1,6 @@
mod graph_algo; mod graph_algo;
pub mod acl_filter;
pub mod peer; pub mod peer;
// pub mod peer_conn; // pub mod peer_conn;
pub mod peer_conn; pub mod peer_conn;

View File

@@ -573,6 +573,8 @@ impl PeerManager {
let foreign_mgr = self.foreign_network_manager.clone(); let foreign_mgr = self.foreign_network_manager.clone();
let encryptor = self.encryptor.clone(); let encryptor = self.encryptor.clone();
let compress_algo = self.data_compress_algo; let compress_algo = self.data_compress_algo;
let acl_filter = self.global_ctx.get_acl_filter().clone();
let global_ctx = self.global_ctx.clone();
self.tasks.lock().await.spawn(async move { self.tasks.lock().await.spawn(async move {
tracing::trace!("start_peer_recv"); tracing::trace!("start_peer_recv");
while let Ok(ret) = recv_packet_from_chan(&mut recv).await { while let Ok(ret) = recv_packet_from_chan(&mut recv).await {
@@ -631,6 +633,15 @@ impl PeerManager {
continue; continue;
} }
if !acl_filter.process_packet_with_acl(
&ret,
true,
global_ctx.get_ipv4().map(|x| x.address()),
global_ctx.get_ipv6().map(|x| x.address()),
) {
continue;
}
let mut processed = false; let mut processed = false;
let mut zc_packet = Some(ret); let mut zc_packet = Some(ret);
let mut idx = 0; let mut idx = 0;
@@ -845,6 +856,14 @@ 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
.global_ctx
.get_acl_filter()
.process_packet_with_acl(data, false, None, None)
{
return;
}
for pipeline in self.nic_packet_process_pipeline.read().await.iter().rev() { for pipeline in self.nic_packet_process_pipeline.read().await.iter().rev() {
let _ = pipeline.try_process_packet_from_nic(data).await; let _ = pipeline.try_process_packet_from_nic(data).await;
} }

View File

@@ -2,10 +2,10 @@ use std::sync::Arc;
use crate::proto::{ use crate::proto::{
cli::{ cli::{
DumpRouteRequest, DumpRouteResponse, ListForeignNetworkRequest, ListForeignNetworkResponse, AclManageRpc, DumpRouteRequest, DumpRouteResponse, GetAclStatsRequest, GetAclStatsResponse,
ListGlobalForeignNetworkRequest, ListGlobalForeignNetworkResponse, ListPeerRequest, ListForeignNetworkRequest, ListForeignNetworkResponse, ListGlobalForeignNetworkRequest,
ListPeerResponse, ListRouteRequest, ListRouteResponse, PeerInfo, PeerManageRpc, ListGlobalForeignNetworkResponse, ListPeerRequest, ListPeerResponse, ListRouteRequest,
ShowNodeInfoRequest, ShowNodeInfoResponse, ListRouteResponse, PeerInfo, PeerManageRpc, ShowNodeInfoRequest, ShowNodeInfoResponse,
}, },
rpc_types::{self, controller::BaseController}, rpc_types::{self, controller::BaseController},
}; };
@@ -134,3 +134,23 @@ impl PeerManageRpc for PeerManagerRpcService {
}) })
} }
} }
#[async_trait::async_trait]
impl AclManageRpc for PeerManagerRpcService {
type Controller = BaseController;
async fn get_acl_stats(
&self,
_: BaseController,
_request: GetAclStatsRequest,
) -> Result<GetAclStatsResponse, rpc_types::error::Error> {
let acl_stats = self
.peer_manager
.get_global_ctx()
.get_acl_filter()
.get_stats();
Ok(GetAclStatsResponse {
acl_stats: Some(acl_stats),
})
}
}

View File

@@ -0,0 +1,127 @@
syntax = "proto3";
import "common.proto";
package acl;
// Enhanced protocol enum with more granular options
enum Protocol {
Unspecified = 0;
TCP = 1;
UDP = 2;
ICMP = 3;
ICMPv6 = 4;
Any = 5;
}
enum Action {
Noop = 0;
Allow = 1;
Drop = 2; // Silent drop (no response)
}
enum ChainType {
UnspecifiedChain = 0;
// send to this node
Inbound = 1;
// send from this node
Outbound = 2;
// subnet proxy
Forward = 3;
}
// Time-based access control
message TimeWindow {
// Days of week: 0=Sunday, 1=Monday, ..., 6=Saturday
repeated uint32 days_of_week = 1;
// Time in minutes from midnight (0-1439)
uint32 start_time = 2;
uint32 end_time = 3;
// Timezone offset in minutes from UTC
int32 timezone_offset = 4;
}
// Enhanced rule with priority and metadata
message Rule {
// Rule identification and metadata
string name = 1; // Human-readable rule name
string description = 2; // Rule description
uint32 priority = 3; // Higher number = higher priority (0-65535)
bool enabled = 4; // Rule enabled/disabled state
// Core matching criteria
Protocol protocol = 5;
repeated string ports = 6;
repeated string source_ips = 7; // Source IP ranges
repeated string destination_ips = 8; // Destination IP ranges
// Enhanced matching criteria
repeated string source_ports = 9; // Source port range
// Action and logging
Action action = 10;
// Rate limiting (packets per second)
uint32 rate_limit = 11; // 0 = no limit
uint32 burst_limit = 12; // Burst allowance
// Connection tracking
bool stateful = 13; // Enable connection tracking
}
// Rule chain with metadata and optimization hints
message Chain {
// Chain identification
string name = 1; // Human-readable chain name
ChainType chain_type = 2;
string description = 3; // Chain description
bool enabled = 4; // Chain enabled/disabled state
// Rules in priority order (highest priority first)
repeated Rule rules = 5;
// Default action when no rules match
Action default_action = 6;
}
message AclV1 { repeated Chain chains = 1; }
enum ConnState {
New = 0;
Established = 1;
Related = 2;
Invalid = 3;
}
// Connection tracking entry for stateful ACLs
message ConnTrackEntry {
common.SocketAddr src_addr = 1;
common.SocketAddr dst_addr = 2;
Protocol protocol = 3; // IP protocol number (e.g., 6 = TCP, 17 = UDP)
ConnState state = 4;
uint64 created_at = 5; // Unix timestamp (seconds)
uint64 last_seen = 6; // Unix timestamp (seconds)
uint64 packet_count = 7;
uint64 byte_count = 8;
}
// Top-level ACL configuration
message Acl {
AclV1 acl_v1 = 2;
}
message StatItem {
uint64 packet_count = 1;
uint64 byte_count = 2;
}
message RuleStats {
Rule rule = 1;
StatItem stat = 2;
}
message AclStats {
repeated RuleStats rules = 1;
repeated ConnTrackEntry conn_track = 2;
map<string, uint64> global = 3;
}

95
easytier/src/proto/acl.rs Normal file
View File

@@ -0,0 +1,95 @@
use std::fmt::Display;
include!(concat!(env!("OUT_DIR"), "/acl.rs"));
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
)
}
}
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(())
}
}

View File

@@ -2,6 +2,7 @@ syntax = "proto3";
import "common.proto"; import "common.proto";
import "peer_rpc.proto"; import "peer_rpc.proto";
import "acl.proto";
package cli; package cli;
@@ -251,3 +252,13 @@ service TcpProxyRpc {
rpc ListTcpProxyEntry(ListTcpProxyEntryRequest) rpc ListTcpProxyEntry(ListTcpProxyEntryRequest)
returns (ListTcpProxyEntryResponse); returns (ListTcpProxyEntryResponse);
} }
message GetAclStatsRequest {}
message GetAclStatsResponse {
acl.AclStats acl_stats = 1;
}
service AclManageRpc {
rpc GetAclStats(GetAclStatsRequest) returns (GetAclStatsResponse);
}

View File

@@ -18,7 +18,8 @@ message FlagsInConfig {
bool disable_p2p = 11; bool disable_p2p = 11;
bool relay_all_peer_rpc = 12; bool relay_all_peer_rpc = 12;
bool disable_udp_hole_punching = 13; bool disable_udp_hole_punching = 13;
// string ipv6_listener = 14; [deprecated = true]; use -l udp://[::]:12345 instead // string ipv6_listener = 14; [deprecated = true]; use -l udp://[::]:12345
// instead
bool multi_thread = 15; bool multi_thread = 15;
CompressionAlgoPb data_compress_algo = 16; CompressionAlgoPb data_compress_algo = 16;
bool bind_device = 17; bool bind_device = 17;
@@ -144,6 +145,13 @@ message Ipv6Inet {
uint32 network_length = 2; uint32 network_length = 2;
} }
message IpInet {
oneof ip {
Ipv4Inet ipv4 = 1;
Ipv6Inet ipv6 = 2;
};
}
message Url { string url = 1; } message Url { string url = 1; }
message SocketAddr { message SocketAddr {
@@ -173,7 +181,7 @@ message PeerFeatureFlag {
bool is_public_server = 1; bool is_public_server = 1;
bool avoid_relay_data = 2; bool avoid_relay_data = 2;
bool kcp_input = 3; bool kcp_input = 3;
bool no_relay_kcp = 4; bool no_relay_kcp = 4;
} }
enum SocketType { enum SocketType {
@@ -182,17 +190,17 @@ enum SocketType {
} }
message PortForwardConfigPb { message PortForwardConfigPb {
SocketAddr bind_addr = 1; SocketAddr bind_addr = 1;
SocketAddr dst_addr = 2; SocketAddr dst_addr = 2;
SocketType socket_type = 3; SocketType socket_type = 3;
} }
message ProxyDstInfo { message ProxyDstInfo { SocketAddr dst_addr = 1; }
SocketAddr dst_addr = 1;
}
message LimiterConfig { message LimiterConfig {
optional uint64 burst_rate = 1; // default 1 means no burst (capacity is same with bps) optional uint64 burst_rate =
1; // default 1 means no burst (capacity is same with bps)
optional uint64 bps = 2; // default 0 means no limit (unit is B/s) optional uint64 bps = 2; // default 0 means no limit (unit is B/s)
optional uint64 fill_duration_ms = 3; // default 10ms, the period to fill the bucket optional uint64 fill_duration_ms =
3; // default 10ms, the period to fill the bucket
} }

View File

@@ -1,4 +1,7 @@
use std::{fmt, str::FromStr}; use std::{
fmt::{self, Display},
str::FromStr,
};
use anyhow::Context; use anyhow::Context;
@@ -166,6 +169,43 @@ impl FromStr for 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.clone()))
}
}
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 { impl From<url::Url> for Url {
fn from(value: url::Url) -> Self { fn from(value: url::Url) -> Self {
Url { Url {

View File

@@ -1,6 +1,7 @@
pub mod rpc_impl; pub mod rpc_impl;
pub mod rpc_types; pub mod rpc_types;
pub mod acl;
pub mod cli; pub mod cli;
pub mod common; pub mod common;
pub mod error; pub mod error;

View File

@@ -1328,3 +1328,183 @@ async fn avoid_tunnel_loop_back_to_virtual_network() {
drop_insts(insts).await; drop_insts(insts).await;
} }
#[tokio::test]
#[serial_test::serial]
pub async fn acl_rule_test_inbound() {
use crate::tunnel::{
common::tests::_tunnel_pingpong_netns,
tcp::{TcpTunnelConnector, TcpTunnelListener},
udp::{UdpTunnelConnector, UdpTunnelListener},
};
use rand::Rng;
let insts = init_three_node("udp").await;
// 构造 ACL 配置
use crate::proto::acl::*;
let mut acl = Acl::default();
let mut acl_v1 = AclV1::default();
let mut chain = Chain::default();
chain.name = "test_inbound".to_string();
chain.chain_type = ChainType::Inbound as i32;
chain.enabled = true;
// 禁止 8080
let mut deny_rule = Rule::default();
deny_rule.name = "deny_8080".to_string();
deny_rule.priority = 200;
deny_rule.enabled = true;
deny_rule.action = Action::Drop as i32;
deny_rule.protocol = Protocol::Any as i32;
deny_rule.ports = vec!["8080".to_string()];
chain.rules.push(deny_rule);
// 允许其他
let mut allow_rule = Rule::default();
allow_rule.name = "allow_all".to_string();
allow_rule.priority = 100;
allow_rule.enabled = true;
allow_rule.action = Action::Allow as i32;
allow_rule.protocol = Protocol::Any as i32;
allow_rule.stateful = true;
chain.rules.push(allow_rule);
// 禁止 src ip 为 10.144.144.2 的流量
let mut deny_rule = Rule::default();
deny_rule.name = "deny_10.144.144.2".to_string();
deny_rule.priority = 200;
deny_rule.enabled = true;
deny_rule.action = Action::Drop as i32;
deny_rule.protocol = Protocol::Any as i32;
deny_rule.source_ips = vec!["10.144.144.2/32".to_string()];
chain.rules.push(deny_rule);
acl_v1.chains.push(chain);
acl.acl_v1 = Some(acl_v1);
// convert acl to to toml
let acl_toml = toml::to_string(&acl).unwrap();
println!("ACL TOML: {}", acl_toml);
insts[2]
.get_global_ctx()
.get_acl_filter()
.reload_rules(Some(&acl));
// TCP 测试部分
{
// 2. 在 inst2 上监听 8080 和 8081
let listener_8080 = TcpTunnelListener::new("tcp://0.0.0.0:8080".parse().unwrap());
let listener_8081 = TcpTunnelListener::new("tcp://0.0.0.0:8081".parse().unwrap());
let listener_8082 = TcpTunnelListener::new("tcp://0.0.0.0:8082".parse().unwrap());
// 3. inst1 作为客户端,尝试连接 inst2 的 8080应被拒绝和 8081应被允许
let connector_8080 =
TcpTunnelConnector::new(format!("tcp://{}:8080", "10.144.144.3").parse().unwrap());
let connector_8081 =
TcpTunnelConnector::new(format!("tcp://{}:8081", "10.144.144.3").parse().unwrap());
let connector_8082 =
TcpTunnelConnector::new(format!("tcp://{}:8082", "10.144.144.3").parse().unwrap());
// 4. 构造测试数据
let mut buf = vec![0; 32];
rand::thread_rng().fill(&mut buf[..]);
// 5. 8081 应该可以 pingpong 成功
_tunnel_pingpong_netns(
listener_8081,
connector_8081,
NetNS::new(Some("net_c".into())),
NetNS::new(Some("net_a".into())),
buf.clone(),
)
.await;
// 6. 8080 应该连接失败(被 ACL 拦截)
let result = tokio::time::timeout(
std::time::Duration::from_millis(200),
_tunnel_pingpong_netns(
listener_8080,
connector_8080,
NetNS::new(Some("net_c".into())),
NetNS::new(Some("net_a".into())),
buf.clone(),
),
)
.await;
assert!(result.is_err(), "TCP 连接 8080 应被 ACL 拦截,不能成功");
// 7. 从 10.144.144.2 连接 8082 应该连接失败(被 ACL 拦截)
let result = tokio::time::timeout(
std::time::Duration::from_millis(200),
_tunnel_pingpong_netns(
listener_8082,
connector_8082,
NetNS::new(Some("net_c".into())),
NetNS::new(Some("net_b".into())),
buf.clone(),
),
)
.await;
assert!(result.is_err(), "TCP 连接 8082 应被 ACL 拦截,不能成功");
let stats = insts[2].get_global_ctx().get_acl_filter().get_stats();
println!("stats: {:?}", stats);
}
// UDP 测试部分
{
// 1. 在 inst2 上监听 UDP 8080 和 8081
let listener_8080 = UdpTunnelListener::new("udp://0.0.0.0:8080".parse().unwrap());
let listener_8081 = UdpTunnelListener::new("udp://0.0.0.0:8081".parse().unwrap());
// 2. inst1 作为客户端,尝试连接 inst2 的 8080应被拒绝和 8081应被允许
let connector_8080 =
UdpTunnelConnector::new(format!("udp://{}:8080", "10.144.144.3").parse().unwrap());
let connector_8081 =
UdpTunnelConnector::new(format!("udp://{}:8081", "10.144.144.3").parse().unwrap());
// 3. 构造测试数据
let mut buf = vec![0; 32];
rand::thread_rng().fill(&mut buf[..]);
// 4. 8081 应该可以 pingpong 成功
_tunnel_pingpong_netns(
listener_8081,
connector_8081,
NetNS::new(Some("net_c".into())),
NetNS::new(Some("net_a".into())),
buf.clone(),
)
.await;
// 5. 8080 应该连接失败(被 ACL 拦截)
let result = tokio::time::timeout(
std::time::Duration::from_millis(200),
_tunnel_pingpong_netns(
listener_8080,
connector_8080,
NetNS::new(Some("net_c".into())),
NetNS::new(Some("net_a".into())),
buf.clone(),
),
)
.await;
assert!(result.is_err(), "UDP 连接 8080 应被 ACL 拦截,不能成功");
let stats = insts[2].get_global_ctx().get_acl_filter().get_stats();
println!("stats: {}", stats);
}
// remove acl, 8080 should succ
insts[2]
.get_global_ctx()
.get_acl_filter()
.reload_rules(None);
drop_insts(insts).await;
}