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:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1955,6 +1955,7 @@ version = "2.3.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"async-recursion",
|
||||
"async-ringbuf",
|
||||
"async-stream",
|
||||
|
||||
@@ -40,6 +40,7 @@ tracing-appender = "0.2.3"
|
||||
thiserror = "1.0"
|
||||
auto_impl = "1.1.0"
|
||||
crossbeam = "0.8.4"
|
||||
arc-swap = "1.7"
|
||||
time = "0.3"
|
||||
toml = "0.8.12"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
@@ -147,6 +147,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"src/proto/cli.proto",
|
||||
"src/proto/web.proto",
|
||||
"src/proto/magic_dns.proto",
|
||||
"src/proto/acl.proto",
|
||||
];
|
||||
|
||||
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();
|
||||
config
|
||||
.protoc_arg("--experimental_allow_proto3_optional")
|
||||
.type_attribute(".acl", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.type_attribute(".cli", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
|
||||
1334
easytier/src/common/acl_processor.rs
Normal file
1334
easytier/src/common/acl_processor.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,10 @@ use cidr::IpCidr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
proto::common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
|
||||
proto::{
|
||||
acl::Acl,
|
||||
common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
|
||||
},
|
||||
tunnel::generate_digest_from_str,
|
||||
};
|
||||
|
||||
@@ -116,6 +119,9 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_port_forwards(&self) -> 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;
|
||||
}
|
||||
|
||||
@@ -291,6 +297,8 @@ struct Config {
|
||||
|
||||
#[serde(skip)]
|
||||
flags_struct: Option<Flags>,
|
||||
|
||||
acl: Option<Acl>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -649,6 +657,14 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
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 {
|
||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||
let default_flags_hashmap =
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
|
||||
use crate::common::config::ProxyNetworkConfig;
|
||||
use crate::common::token_bucket::TokenBucketManager;
|
||||
use crate::peers::acl_filter::AclFilter;
|
||||
use crate::proto::cli::PeerConnInfo;
|
||||
use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
@@ -81,6 +82,8 @@ pub struct GlobalCtx {
|
||||
quic_proxy_port: AtomicCell<Option<u16>>,
|
||||
|
||||
token_bucket_manager: TokenBucketManager,
|
||||
|
||||
acl_filter: Arc<AclFilter>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GlobalCtx {
|
||||
@@ -108,7 +111,7 @@ impl GlobalCtx {
|
||||
|
||||
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 no_tun = config_fs.get_flags().no_tun;
|
||||
|
||||
@@ -147,6 +150,8 @@ impl GlobalCtx {
|
||||
quic_proxy_port: AtomicCell::new(None),
|
||||
|
||||
token_bucket_manager: TokenBucketManager::new(),
|
||||
|
||||
acl_filter: Arc::new(AclFilter::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +322,10 @@ impl GlobalCtx {
|
||||
pub fn token_bucket_manager(&self) -> &TokenBucketManager {
|
||||
&self.token_bucket_manager
|
||||
}
|
||||
|
||||
pub fn get_acl_filter(&self) -> &Arc<AclFilter> {
|
||||
&self.acl_filter
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -10,6 +10,7 @@ use tracing::Instrument;
|
||||
|
||||
use crate::{set_global_var, use_global_var};
|
||||
|
||||
pub mod acl_processor;
|
||||
pub mod compressor;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
|
||||
@@ -27,11 +27,12 @@ use easytier::{
|
||||
},
|
||||
proto::{
|
||||
cli::{
|
||||
list_peer_route_pair, ConnectorManageRpc, ConnectorManageRpcClientFactory,
|
||||
DumpRouteRequest, GetVpnPortalInfoRequest, ListConnectorRequest,
|
||||
ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest,
|
||||
ListPeerRequest, ListPeerResponse, ListRouteRequest, ListRouteResponse,
|
||||
ManageMappedListenerRequest, MappedListenerManageAction, MappedListenerManageRpc,
|
||||
list_peer_route_pair, AclManageRpc, AclManageRpcClientFactory, ConnectorManageRpc,
|
||||
ConnectorManageRpcClientFactory, DumpRouteRequest, GetAclStatsRequest,
|
||||
GetVpnPortalInfoRequest, ListConnectorRequest, ListForeignNetworkRequest,
|
||||
ListGlobalForeignNetworkRequest, ListMappedListenerRequest, ListPeerRequest,
|
||||
ListPeerResponse, ListRouteRequest, ListRouteResponse, ManageMappedListenerRequest,
|
||||
MappedListenerManageAction, MappedListenerManageRpc,
|
||||
MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc,
|
||||
PeerManageRpcClientFactory, ShowNodeInfoRequest, TcpProxyEntryState,
|
||||
TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc,
|
||||
@@ -93,6 +94,8 @@ enum SubCommand {
|
||||
Service(ServiceArgs),
|
||||
#[command(about = "show tcp/kcp proxy status")]
|
||||
Proxy,
|
||||
#[command(about = "show ACL rules statistics")]
|
||||
Acl(AclArgs),
|
||||
#[command(about = t!("core_clap.generate_completions").to_string())]
|
||||
GenAutocomplete { shell: Shell },
|
||||
}
|
||||
@@ -179,6 +182,17 @@ struct NodeArgs {
|
||||
sub_command: Option<NodeSubCommand>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct AclArgs {
|
||||
#[command(subcommand)]
|
||||
sub_command: Option<AclSubCommand>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum AclSubCommand {
|
||||
Stats,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct ServiceArgs {
|
||||
#[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")?)
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
transport_type: &str,
|
||||
@@ -688,6 +714,26 @@ impl CommandHandler<'_> {
|
||||
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> {
|
||||
let client = self.get_mapped_listener_manager_client().await?;
|
||||
let request = ListMappedListenerRequest::default();
|
||||
@@ -1443,6 +1489,11 @@ async fn main() -> Result<(), Error> {
|
||||
|
||||
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 } => {
|
||||
let mut cmd = Cli::command();
|
||||
easytier::print_completions(shell, &mut cmd, "easytier-cli");
|
||||
|
||||
@@ -29,7 +29,10 @@ use easytier::{
|
||||
connector::create_connector_by_url,
|
||||
instance_manager::NetworkInstanceManager,
|
||||
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},
|
||||
utils::{init_logger, setup_panic_handler},
|
||||
web_client,
|
||||
@@ -506,6 +509,22 @@ struct NetworkOptions {
|
||||
help = t!("core_clap.foreign_relay_bps_limit").to_string(),
|
||||
)]
|
||||
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)]
|
||||
@@ -603,6 +622,117 @@ impl NetworkOptions {
|
||||
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<()> {
|
||||
if self.hostname.is_some() {
|
||||
cfg.set_hostname(self.hostname.clone());
|
||||
@@ -860,6 +990,11 @@ impl NetworkOptions {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +298,10 @@ impl NicPacketFilter for MagicDnsServerInstanceData {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
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");
|
||||
Ok(tunnel_info)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
self.ip_proxy = Some(IpProxy::new(
|
||||
self.get_global_ctx(),
|
||||
@@ -801,10 +805,11 @@ impl Instance {
|
||||
let mapped_listener_manager_rpc = self.get_mapped_listener_manager_rpc_service();
|
||||
|
||||
let s = self.rpc_server.as_mut().unwrap();
|
||||
s.registry().register(
|
||||
PeerManageRpcServer::new(PeerManagerRpcService::new(peer_mgr)),
|
||||
"",
|
||||
);
|
||||
let peer_mgr_rpc_service = PeerManagerRpcService::new(peer_mgr.clone());
|
||||
s.registry()
|
||||
.register(PeerManageRpcServer::new(peer_mgr_rpc_service.clone()), "");
|
||||
s.registry()
|
||||
.register(AclManageRpcServer::new(peer_mgr_rpc_service), "");
|
||||
s.registry().register(
|
||||
ConnectorManageRpcServer::new(ConnectorManagerRpcService(conn_manager)),
|
||||
"",
|
||||
|
||||
289
easytier/src/peers/acl_filter.rs
Normal file
289
easytier/src/peers/acl_filter.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod graph_algo;
|
||||
|
||||
pub mod acl_filter;
|
||||
pub mod peer;
|
||||
// pub mod peer_conn;
|
||||
pub mod peer_conn;
|
||||
|
||||
@@ -573,6 +573,8 @@ impl PeerManager {
|
||||
let foreign_mgr = self.foreign_network_manager.clone();
|
||||
let encryptor = self.encryptor.clone();
|
||||
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 {
|
||||
tracing::trace!("start_peer_recv");
|
||||
while let Ok(ret) = recv_packet_from_chan(&mut recv).await {
|
||||
@@ -631,6 +633,15 @@ impl PeerManager {
|
||||
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 zc_packet = Some(ret);
|
||||
let mut idx = 0;
|
||||
@@ -845,6 +856,14 @@ impl PeerManager {
|
||||
}
|
||||
|
||||
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() {
|
||||
let _ = pipeline.try_process_packet_from_nic(data).await;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use std::sync::Arc;
|
||||
|
||||
use crate::proto::{
|
||||
cli::{
|
||||
DumpRouteRequest, DumpRouteResponse, ListForeignNetworkRequest, ListForeignNetworkResponse,
|
||||
ListGlobalForeignNetworkRequest, ListGlobalForeignNetworkResponse, ListPeerRequest,
|
||||
ListPeerResponse, ListRouteRequest, ListRouteResponse, PeerInfo, PeerManageRpc,
|
||||
ShowNodeInfoRequest, ShowNodeInfoResponse,
|
||||
AclManageRpc, DumpRouteRequest, DumpRouteResponse, GetAclStatsRequest, GetAclStatsResponse,
|
||||
ListForeignNetworkRequest, ListForeignNetworkResponse, ListGlobalForeignNetworkRequest,
|
||||
ListGlobalForeignNetworkResponse, ListPeerRequest, ListPeerResponse, ListRouteRequest,
|
||||
ListRouteResponse, PeerInfo, PeerManageRpc, ShowNodeInfoRequest, ShowNodeInfoResponse,
|
||||
},
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
127
easytier/src/proto/acl.proto
Normal file
127
easytier/src/proto/acl.proto
Normal 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
95
easytier/src/proto/acl.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ syntax = "proto3";
|
||||
|
||||
import "common.proto";
|
||||
import "peer_rpc.proto";
|
||||
import "acl.proto";
|
||||
|
||||
package cli;
|
||||
|
||||
@@ -251,3 +252,13 @@ service TcpProxyRpc {
|
||||
rpc ListTcpProxyEntry(ListTcpProxyEntryRequest)
|
||||
returns (ListTcpProxyEntryResponse);
|
||||
}
|
||||
|
||||
message GetAclStatsRequest {}
|
||||
|
||||
message GetAclStatsResponse {
|
||||
acl.AclStats acl_stats = 1;
|
||||
}
|
||||
|
||||
service AclManageRpc {
|
||||
rpc GetAclStats(GetAclStatsRequest) returns (GetAclStatsResponse);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ message FlagsInConfig {
|
||||
bool disable_p2p = 11;
|
||||
bool relay_all_peer_rpc = 12;
|
||||
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;
|
||||
CompressionAlgoPb data_compress_algo = 16;
|
||||
bool bind_device = 17;
|
||||
@@ -144,6 +145,13 @@ message Ipv6Inet {
|
||||
uint32 network_length = 2;
|
||||
}
|
||||
|
||||
message IpInet {
|
||||
oneof ip {
|
||||
Ipv4Inet ipv4 = 1;
|
||||
Ipv6Inet ipv6 = 2;
|
||||
};
|
||||
}
|
||||
|
||||
message Url { string url = 1; }
|
||||
|
||||
message SocketAddr {
|
||||
@@ -173,7 +181,7 @@ message PeerFeatureFlag {
|
||||
bool is_public_server = 1;
|
||||
bool avoid_relay_data = 2;
|
||||
bool kcp_input = 3;
|
||||
bool no_relay_kcp = 4;
|
||||
bool no_relay_kcp = 4;
|
||||
}
|
||||
|
||||
enum SocketType {
|
||||
@@ -182,17 +190,17 @@ enum SocketType {
|
||||
}
|
||||
|
||||
message PortForwardConfigPb {
|
||||
SocketAddr bind_addr = 1;
|
||||
SocketAddr dst_addr = 2;
|
||||
SocketType socket_type = 3;
|
||||
SocketAddr bind_addr = 1;
|
||||
SocketAddr dst_addr = 2;
|
||||
SocketType socket_type = 3;
|
||||
}
|
||||
|
||||
message ProxyDstInfo {
|
||||
SocketAddr dst_addr = 1;
|
||||
}
|
||||
message ProxyDstInfo { SocketAddr dst_addr = 1; }
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::{fmt, str::FromStr};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
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 {
|
||||
fn from(value: url::Url) -> Self {
|
||||
Url {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod rpc_impl;
|
||||
pub mod rpc_types;
|
||||
|
||||
pub mod acl;
|
||||
pub mod cli;
|
||||
pub mod common;
|
||||
pub mod error;
|
||||
|
||||
@@ -1328,3 +1328,183 @@ async fn avoid_tunnel_loop_back_to_virtual_network() {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user