From 5b502f593b819df8846f9a92856dbe700fd09def Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 09:32:02 +0000 Subject: [PATCH 1/8] Initial plan From 137b128482236410530a041c187154af690bff64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 09:38:42 +0000 Subject: [PATCH 2/8] Add filter drop capability to nat-common and nat-cli Co-authored-by: arloor <21768987+arloor@users.noreply.github.com> --- nat-cli/src/config.rs | 166 ++++++++++++++++++++++-- nat-cli/src/main.rs | 14 +++ nat-common/src/lib.rs | 286 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 459 insertions(+), 7 deletions(-) diff --git a/nat-cli/src/config.rs b/nat-cli/src/config.rs index 0e054c4..746f4ef 100644 --- a/nat-cli/src/config.rs +++ b/nat-cli/src/config.rs @@ -1,17 +1,18 @@ #![deny(warnings)] use crate::ip; use log::info; -use nat_common::{IpVersion, NftCell, ParseError, Protocol, TomlConfig}; +use nat_common::{FilterRule, IpVersion, NftCell, ParseError, Protocol, TomlConfig, Chain}; use std::env; use std::fmt::Display; use std::fs; use std::io; -/// 运行时Cell,包装NftCell和Comment +/// 运行时Cell,包装NftCell、FilterRule和Comment /// Comment仅用于运行时表示,不进入TOML配置 #[derive(Debug)] pub enum RuntimeCell { Rule(NftCell), + Filter(FilterRule), Comment(String), } @@ -19,6 +20,7 @@ impl Display for RuntimeCell { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RuntimeCell::Rule(cell) => write!(f, "{}", cell), + RuntimeCell::Filter(filter) => write!(f, "{}", filter), RuntimeCell::Comment(content) => write!(f, "{}", content), } } @@ -110,11 +112,97 @@ impl RuntimeCell { pub fn build(&self) -> Result { match self { RuntimeCell::Rule(cell) => cell.build(), + RuntimeCell::Filter(filter) => build_filter_rule(filter), RuntimeCell::Comment(content) => Ok(content.clone() + "\n"), } } } +/// 构建过滤规则的nftables脚本 +fn build_filter_rule(filter: &FilterRule) -> Result { + let mut result = String::new(); + + match filter.ip_version { + IpVersion::All => { + result += &build_filter_rule_for_family(filter, &IpVersion::V4)?; + result += &build_filter_rule_for_family(filter, &IpVersion::V6)?; + } + _ => { + result += &build_filter_rule_for_family(filter, &filter.ip_version)?; + } + } + + Ok(result) +} + +/// 为特定IP family构建过滤规则 +fn build_filter_rule_for_family(filter: &FilterRule, ip_version: &IpVersion) -> Result { + let (family, ip_prefix) = match ip_version { + IpVersion::V4 => ("ip", "ip"), + IpVersion::V6 => ("ip6", "ip6"), + IpVersion::All => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "IpVersion::All should be handled at caller level", + )); + } + }; + + let chain_name = match filter.chain { + Chain::Input => "INPUT", + Chain::Forward => "FORWARD", + }; + + let mut conditions = Vec::new(); + + // 添加协议条件 + if filter.protocol != Protocol::All || filter.src_port.is_some() || filter.dst_port.is_some() { + let proto = filter.protocol.nft_proto(); + conditions.push(proto.to_string()); + } + + // 添加源IP条件 + if let Some(ref src_ip) = filter.src_ip { + conditions.push(format!("{} saddr {}", ip_prefix, src_ip)); + } + + // 添加目标IP条件 + if let Some(ref dst_ip) = filter.dst_ip { + conditions.push(format!("{} daddr {}", ip_prefix, dst_ip)); + } + + // 添加源端口条件 + if let Some(src_port) = filter.src_port { + if let Some(end) = filter.src_port_end { + conditions.push(format!("sport {}-{}", src_port, end)); + } else { + conditions.push(format!("sport {}", src_port)); + } + } + + // 添加目标端口条件 + if let Some(dst_port) = filter.dst_port { + if let Some(end) = filter.dst_port_end { + conditions.push(format!("dport {}-{}", dst_port, end)); + } else { + conditions.push(format!("dport {}", dst_port)); + } + } + + let conditions_str = conditions.join(" "); + let comment_str = if let Some(ref comment) = filter.comment { + format!(" comment \"{}\"", comment) + } else { + format!(" comment \"{}\"", filter) + }; + + let rule = format!( + "add rule {family} self-filter {chain_name} {conditions_str} counter drop{comment_str}\n\n" + ); + + Ok(rule) +} + fn build_nat_rules(cell: &NftCell, dst_ip: &str, ip_version: &IpVersion) -> Result { let (family, env_var, localhost_addr, fmt_ip) = match ip_version { IpVersion::V4 => ("ip", "nat_local_ip", "127.0.0.1", dst_ip.to_string()), @@ -242,7 +330,7 @@ fn build_redirect_rule(cell: &NftCell, ip_version: &IpVersion) -> Result Option { let line = line.trim(); @@ -251,7 +339,17 @@ fn parse_legacy_line(line: &str) -> Option { return Some(RuntimeCell::Comment(line.to_string())); } - // 使用 nat-common 的 TryFrom 解析 + // 先尝试解析为FilterRule + match FilterRule::try_from(line) { + Ok(filter) => return Some(RuntimeCell::Filter(filter)), + Err(ParseError::Skip) => {} // 不是Filter规则,继续尝试其他类型 + Err(ParseError::InvalidFormat(msg)) => { + log::warn!("跳过无效的过滤规则: {}", msg); + return None; + } + } + + // 使用 nat-common 的 TryFrom 解析NAT规则 match NftCell::try_from(line) { Ok(cell) => Some(RuntimeCell::Rule(cell)), Err(ParseError::Skip) => None, @@ -269,12 +367,18 @@ pub(crate) fn example(conf: &str) { "SINGLE,10000,443,baidu.com,all,ipv4\n\ RANGE,1000,2000,baidu.com,tcp,ipv6\n\ REDIRECT,8000,3128,all,ipv4\n\ - REDIRECT,8000-9000,3128,tcp,both\n\ + REDIRECT,8000-9000,3128,tcp,all\n\ + FILTER,input,src_ip=180.213.132.211,all,ipv4\n\ + FILTER,input,src_ip=240e:328:1301::/48,all,ipv6\n\ + FILTER,forward,dst_port=22,tcp,all\n\ # 格式: TYPE,port(s),port/domain,protocol,ip_version\n\ - # TYPE: SINGLE, RANGE, 或 REDIRECT\n\ + # TYPE: SINGLE, RANGE, REDIRECT 或 FILTER\n\ # REDIRECT格式: REDIRECT,src_port,dst_port 或 REDIRECT,src_port-src_port_end,dst_port\n\ + # FILTER格式: FILTER,chain,key=value,...,protocol,ip_version\n\ + # chain: input 或 forward\n\ + # key=value: src_ip=IP, dst_ip=IP, src_port=PORT, dst_port=PORT\n\ # protocol: tcp, udp, all\n\ - # ip_version: ipv4, ipv6, both" + # ip_version: ipv4, ipv6, all" ) } @@ -301,6 +405,7 @@ pub fn read_toml_config(toml_path: &str) -> Result, io::Error> let mut cells = Vec::new(); + // 处理NAT规则 for rule in config.rules { // 如果有注释,先添加注释 let comment = match &rule { @@ -316,6 +421,15 @@ pub fn read_toml_config(toml_path: &str) -> Result, io::Error> cells.push(RuntimeCell::Rule(rule)); } + // 处理过滤规则 + for filter in config.filters { + if let Some(ref comment_text) = filter.comment { + cells.push(RuntimeCell::Comment(format!("# {comment_text}"))); + } + + cells.push(RuntimeCell::Filter(filter)); + } + Ok(cells) } @@ -356,6 +470,44 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { comment: Some("端口范围重定向到本机示例".to_string()), }, ], + filters: vec![ + FilterRule { + chain: Chain::Input, + src_ip: Some("180.213.132.211".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V4, + comment: Some("阻止特定IPv4地址".to_string()), + }, + FilterRule { + chain: Chain::Input, + src_ip: Some("240e:328:1301::/48".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V6, + comment: Some("阻止IPv6网段".to_string()), + }, + FilterRule { + chain: Chain::Input, + src_ip: None, + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: Some(22), + dst_port_end: None, + protocol: Protocol::Tcp, + ip_version: IpVersion::All, + comment: Some("阻止SSH端口访问".to_string()), + }, + ], }; let toml_str = example_config diff --git a/nat-cli/src/main.rs b/nat-cli/src/main.rs index bf10cce..87b628e 100644 --- a/nat-cli/src/main.rs +++ b/nat-cli/src/main.rs @@ -166,6 +166,20 @@ fn build_new_script(nat_cells: &[config::RuntimeCell]) -> Result) -> std::fmt::Result { + match self { + Chain::Input => write!(f, "input"), + Chain::Forward => write!(f, "forward"), + } + } +} + +impl From for Chain { + fn from(chain: String) -> Self { + match chain.to_lowercase().as_str() { + "input" => Chain::Input, + "forward" => Chain::Forward, + _ => Chain::Input, + } + } +} + +impl From<&str> for Chain { + fn from(chain: &str) -> Self { + match chain.to_lowercase().as_str() { + "input" => Chain::Input, + "forward" => Chain::Forward, + _ => Chain::Input, + } + } +} + +impl Serialize for Chain { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Chain { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(Chain::from(s)) + } +} + impl Display for Protocol { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -171,7 +227,66 @@ impl<'de> Deserialize<'de> for Protocol { // TOML配置结构定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TomlConfig { + #[serde(default)] pub rules: Vec, + #[serde(default)] + pub filters: Vec, +} + +// Filter规则定义 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FilterRule { + #[serde(default)] + pub chain: Chain, + #[serde(skip_serializing_if = "Option::is_none")] + pub src_ip: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_ip: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub src_port: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub src_port_end: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_port: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_port_end: Option, + #[serde(default)] + pub protocol: Protocol, + #[serde(default)] + pub ip_version: IpVersion, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub comment: Option, +} + +impl Display for FilterRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut parts = vec![format!("FILTER,{}", self.chain)]; + + if let Some(ref ip) = self.src_ip { + parts.push(format!("src_ip={}", ip)); + } + if let Some(ref ip) = self.dst_ip { + parts.push(format!("dst_ip={}", ip)); + } + if let Some(port) = self.src_port { + if let Some(end) = self.src_port_end { + parts.push(format!("src_port={}-{}", port, end)); + } else { + parts.push(format!("src_port={}", port)); + } + } + if let Some(port) = self.dst_port { + if let Some(end) = self.dst_port_end { + parts.push(format!("dst_port={}-{}", port, end)); + } else { + parts.push(format!("dst_port={}", port)); + } + } + parts.push(format!("{}", self.protocol)); + parts.push(format!("{}", self.ip_version)); + + write!(f, "{}", parts.join(",")) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -274,6 +389,10 @@ impl TomlConfig { rule.validate() .map_err(|e| format!("规则 {} 验证失败: {}", idx + 1, e))?; } + for (idx, filter) in self.filters.iter().enumerate() { + filter.validate() + .map_err(|e| format!("过滤规则 {} 验证失败: {}", idx + 1, e))?; + } Ok(()) } @@ -307,6 +426,11 @@ impl TryFrom<&str> for NftCell { let cells: Vec<&str> = line.split(',').collect(); let rule_type = cells.first().map(|s| s.trim()).unwrap_or(""); + // 如果是FILTER类型,返回Skip让FilterRule处理 + if rule_type == "FILTER" { + return Err(ParseError::Skip); + } + // 验证字段数量 match rule_type { "REDIRECT" => { @@ -420,6 +544,119 @@ impl TryFrom<&str> for NftCell { } } +impl TryFrom<&str> for FilterRule { + type Error = ParseError; + + /// 从legacy格式行解析FilterRule + /// 格式: FILTER,chain,key=value,key=value,...,protocol,ip_version + /// 示例: FILTER,input,src_ip=192.168.1.1,tcp,ipv4 + /// 示例: FILTER,forward,dst_port=80-443,src_ip=10.0.0.0/24,tcp,all + fn try_from(line: &str) -> Result { + let line = line.trim(); + + // 处理注释和空行 + if line.is_empty() || line.starts_with('#') { + return Err(ParseError::Skip); + } + + let cells: Vec<&str> = line.split(',').collect(); + let rule_type = cells.first().map(|s| s.trim()).unwrap_or(""); + + if rule_type != "FILTER" { + return Err(ParseError::Skip); + } + + if cells.len() < 3 { + return Err(ParseError::InvalidFormat(format!( + "无效的过滤规则: {line}, FILTER类型至少需要3个字段" + ))); + } + + let chain: Chain = cells[1].trim().into(); + + let mut src_ip: Option = None; + let mut dst_ip: Option = None; + let mut src_port: Option = None; + let mut src_port_end: Option = None; + let mut dst_port: Option = None; + let mut dst_port_end: Option = None; + let mut protocol = Protocol::All; + let mut ip_version = IpVersion::All; + + // 解析key=value对和其他参数 + for i in 2..cells.len() { + let cell = cells[i].trim(); + + // 检查是否是协议 + if cell == "tcp" || cell == "udp" || cell == "all" { + protocol = cell.into(); + continue; + } + + // 检查是否是IP版本 + if cell == "ipv4" || cell == "ipv6" { + ip_version = cell.into(); + continue; + } + + // 解析key=value + if let Some(eq_pos) = cell.find('=') { + let key = &cell[..eq_pos]; + let value = &cell[eq_pos + 1..]; + + match key { + "src_ip" => src_ip = Some(value.to_string()), + "dst_ip" => dst_ip = Some(value.to_string()), + "src_port" => { + if value.contains('-') { + let parts: Vec<&str> = value.split('-').collect(); + if parts.len() != 2 { + return Err(ParseError::InvalidFormat(format!( + "无效的端口范围格式: {value}" + ))); + } + src_port = Some(parts[0].parse::()?); + src_port_end = Some(parts[1].parse::()?); + } else { + src_port = Some(value.parse::()?); + } + } + "dst_port" => { + if value.contains('-') { + let parts: Vec<&str> = value.split('-').collect(); + if parts.len() != 2 { + return Err(ParseError::InvalidFormat(format!( + "无效的端口范围格式: {value}" + ))); + } + dst_port = Some(parts[0].parse::()?); + dst_port_end = Some(parts[1].parse::()?); + } else { + dst_port = Some(value.parse::()?); + } + } + _ => return Err(ParseError::InvalidFormat(format!( + "未知的过滤参数: {key}" + ))), + } + } + } + + Ok(FilterRule { + chain, + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + protocol, + ip_version, + comment: None, + }) + } +} + impl NftCell { /// 验证单个规则是否合法 pub fn validate(&self) -> Result<(), String> { @@ -474,6 +711,55 @@ impl NftCell { } } +impl FilterRule { + /// 验证过滤规则是否合法 + pub fn validate(&self) -> Result<(), String> { + // 至少需要指定一个过滤条件 + if self.src_ip.is_none() + && self.dst_ip.is_none() + && self.src_port.is_none() + && self.dst_port.is_none() { + return Err("至少需要指定一个过滤条件(源IP、目标IP、源端口或目标端口)".to_string()); + } + + // 验证端口范围 + if let Some(port) = self.src_port { + validate_port(port)?; + if let Some(end) = self.src_port_end { + validate_port(end)?; + if port >= end { + return Err(format!("源端口起始 {} 必须小于结束端口 {}", port, end)); + } + } + } + + if let Some(port) = self.dst_port { + validate_port(port)?; + if let Some(end) = self.dst_port_end { + validate_port(end)?; + if port >= end { + return Err(format!("目标端口起始 {} 必须小于结束端口 {}", port, end)); + } + } + } + + // 验证IP地址格式(基础验证) + if let Some(ref ip) = self.src_ip { + if ip.trim().is_empty() { + return Err("源IP不能为空".to_string()); + } + } + + if let Some(ref ip) = self.dst_ip { + if ip.trim().is_empty() { + return Err("目标IP不能为空".to_string()); + } + } + + Ok(()) + } +} + fn validate_port(port: u16) -> Result<(), String> { if port == 0 { return Err("端口号不能为0".to_string()); From 94d643afefb6411c47d137dc3a0ed5a64ffb90c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 09:45:56 +0000 Subject: [PATCH 3/8] Refactor FilterRule as NftCell variant and update setup.sh ExecStop Co-authored-by: arloor <21768987+arloor@users.noreply.github.com> --- README.md | 62 +++++- nat-cli/src/config.rs | 232 +++++++++++---------- nat-common/src/lib.rs | 424 ++++++++++++++++++-------------------- nat-console/src/config.rs | 41 +++- setup.sh | 2 +- 5 files changed, 418 insertions(+), 343 deletions(-) diff --git a/README.md b/README.md index d88e19d..0f294e3 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ ## ✨ 核心特性 - 🔄 **动态 NAT 转发**:自动监测配置文件和目标域名 IP 变化,实时更新转发规则 -- 🌐 **IPv4/IPv6 双栈支持**:完整支持 IPv4 和 IPv6 NAT 转发 +- 🛡️ **防火墙过滤**:支持 Filter Drop 功能,实现类似防火墙的黑名单过滤(INPUT/FORWARD链) +- 🌐 **IPv4/IPv6 双栈支持**:完整支持 IPv4 和 IPv6 NAT 转发和过滤 - 📝 **灵活配置**:支持传统配置文件和 TOML 格式,满足不同使用场景 -- 🎯 **精准控制**:支持单端口、端口段、TCP/UDP 协议选择 +- 🎯 **精准控制**:支持单端口、端口段、TCP/UDP 协议选择、IP地址和网段过滤 - 🔌 **本地重定向**:支持端口重定向到本机其他端口 - 🐋 **Docker 兼容**:与 Docker 网络完美兼容 - ⚡ **高性能轻量**:基于 Rust 编写,仅依赖标准库和少量核心库 @@ -123,7 +124,7 @@ systemctl restart nat-console - ✅ 结构化配置,可读性更好 ```toml -# ============ 基础转发示例 ============ +# ============ NAT 转发规则 ============ # 1. 单端口转发 - HTTPS 流量转发 [[rules]] @@ -155,7 +156,7 @@ protocol = "udp" # 仅 UDP 协议 ip_version = "ipv4" comment = "DNS 查询转发" -# ============ 本地重定向示例 ============ +# ============ 本地重定向规则 ============ # 4. 单端口重定向到本机服务 [[rules]] @@ -176,9 +177,58 @@ protocol = "tcp" ip_version = "all" comment = "批量端口重定向到本机" +# ============ 防火墙过滤规则 (Filter Drop) ============ + +# 6. 阻止特定 IPv4 地址访问 +[[rules]] +type = "filter" +chain = "input" # 链类型: input 或 forward +src_ip = "180.213.132.211" # 源 IP 地址 +protocol = "all" # 协议: all, tcp 或 udp +ip_version = "ipv4" # IP 版本: ipv4, ipv6 或 all +comment = "阻止恶意 IP 访问" + +# 7. 阻止 IPv6 网段访问 +[[rules]] +type = "filter" +chain = "input" +src_ip = "240e:328:1301::/48" # IPv6 网段 +protocol = "all" +ip_version = "ipv6" +comment = "阻止 IPv6 网段访问" + +# 8. 阻止特定端口(如 SSH) +[[rules]] +type = "filter" +chain = "input" +dst_port = 22 # 目标端口 +protocol = "tcp" +ip_version = "all" +comment = "阻止 SSH 端口访问" + +# 9. 阻止端口范围 +[[rules]] +type = "filter" +chain = "forward" +dst_port = 1000 # 起始端口 +dst_port_end = 2000 # 结束端口 +protocol = "tcp" +ip_version = "ipv4" +comment = "阻止转发到端口范围 1000-2000" + +# 10. 组合过滤:特定IP访问特定端口 +[[rules]] +type = "filter" +chain = "input" +src_ip = "192.168.1.0/24" # 源 IP 网段 +dst_port = 3306 # 目标端口 (MySQL) +protocol = "tcp" +ip_version = "ipv4" +comment = "阻止内网访问 MySQL" + # ============ 高级场景示例 ============ -# 6. 强制 IPv6 转发 +# 11. 强制 IPv6 转发 [[rules]] type = "single" sport = 9001 @@ -188,7 +238,7 @@ protocol = "all" ip_version = "ipv6" # 仅使用 IPv6 进行转发 comment = "IPv6 专用服务" -# 7. 双栈支持示例 - 自动选择 IPv4/IPv6 +# 12. 双栈支持示例 - 自动选择 IPv4/IPv6 [[rules]] type = "single" sport = 10080 diff --git a/nat-cli/src/config.rs b/nat-cli/src/config.rs index 746f4ef..648c8af 100644 --- a/nat-cli/src/config.rs +++ b/nat-cli/src/config.rs @@ -1,18 +1,17 @@ #![deny(warnings)] use crate::ip; use log::info; -use nat_common::{FilterRule, IpVersion, NftCell, ParseError, Protocol, TomlConfig, Chain}; +use nat_common::{Chain, IpVersion, NftCell, ParseError, Protocol, TomlConfig}; use std::env; use std::fmt::Display; use std::fs; use std::io; -/// 运行时Cell,包装NftCell、FilterRule和Comment +/// 运行时Cell,包装NftCell和Comment /// Comment仅用于运行时表示,不进入TOML配置 #[derive(Debug)] pub enum RuntimeCell { Rule(NftCell), - Filter(FilterRule), Comment(String), } @@ -20,7 +19,6 @@ impl Display for RuntimeCell { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RuntimeCell::Rule(cell) => write!(f, "{}", cell), - RuntimeCell::Filter(filter) => write!(f, "{}", filter), RuntimeCell::Comment(content) => write!(f, "{}", content), } } @@ -51,60 +49,66 @@ pub trait NftCellBuilder { impl NftCellBuilder for NftCell { fn build(&self) -> Result { - let (domain, ip_version) = match &self { - NftCell::Single { - domain, - ip_version, - .. - } => (domain, ip_version), - NftCell::Range { - domain, - ip_version, - .. - } => (domain, ip_version), - NftCell::Redirect { ip_version, .. } => { - // Redirect doesn't need domain resolution - return build_redirect_rules(self, ip_version); - } - }; - - // 根据配置的IP版本解析目标IP - let dst_ip = ip::remote_ip(domain, ip_version)?; - - let mut result = String::new(); - - // 检测实际IP类型并生成相应的规则 - let is_ipv6_target = dst_ip.contains(':'); - - match ip_version { - IpVersion::V4 => { - if is_ipv6_target { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "IPv6 target address resolved but rule is configured for IPv4 only", - )); - } - result += &build_nat_rules(self, &dst_ip, &IpVersion::V4)?; - } - IpVersion::V6 => { - if !is_ipv6_target { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "IPv4 target address resolved but rule is configured for IPv6 only", - )); - } - result += &build_nat_rules(self, &dst_ip, &IpVersion::V6)?; - } - IpVersion::All => { - if is_ipv6_target { - result += &build_nat_rules(self, &dst_ip, &IpVersion::V6)?; - } else { - result += &build_nat_rules(self, &dst_ip, &IpVersion::V4)?; + match self { + NftCell::Filter { .. } => build_filter_rule(self), + _ => { + let (domain, ip_version) = match &self { + NftCell::Single { + domain, + ip_version, + .. + } => (domain, ip_version), + NftCell::Range { + domain, + ip_version, + .. + } => (domain, ip_version), + NftCell::Redirect { ip_version, .. } => { + // Redirect doesn't need domain resolution + return build_redirect_rules(self, ip_version); + } + NftCell::Filter { .. } => unreachable!(), + }; + + // 根据配置的IP版本解析目标IP + let dst_ip = ip::remote_ip(domain, ip_version)?; + + let mut result = String::new(); + + // 检测实际IP类型并生成相应的规则 + let is_ipv6_target = dst_ip.contains(':'); + + match ip_version { + IpVersion::V4 => { + if is_ipv6_target { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "IPv6 target address resolved but rule is configured for IPv4 only", + )); + } + result += &build_nat_rules(self, &dst_ip, &IpVersion::V4)?; + } + IpVersion::V6 => { + if !is_ipv6_target { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "IPv4 target address resolved but rule is configured for IPv6 only", + )); + } + result += &build_nat_rules(self, &dst_ip, &IpVersion::V6)?; + } + IpVersion::All => { + if is_ipv6_target { + result += &build_nat_rules(self, &dst_ip, &IpVersion::V6)?; + } else { + result += &build_nat_rules(self, &dst_ip, &IpVersion::V4)?; + } + } } + + Ok(result) } } - - Ok(result) } } @@ -112,23 +116,40 @@ impl RuntimeCell { pub fn build(&self) -> Result { match self { RuntimeCell::Rule(cell) => cell.build(), - RuntimeCell::Filter(filter) => build_filter_rule(filter), RuntimeCell::Comment(content) => Ok(content.clone() + "\n"), } } } /// 构建过滤规则的nftables脚本 -fn build_filter_rule(filter: &FilterRule) -> Result { +fn build_filter_rule(cell: &NftCell) -> Result { + let NftCell::Filter { + chain, + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + protocol, + ip_version, + comment, + } = cell else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Expected Filter cell", + )); + }; + let mut result = String::new(); - match filter.ip_version { + match ip_version { IpVersion::All => { - result += &build_filter_rule_for_family(filter, &IpVersion::V4)?; - result += &build_filter_rule_for_family(filter, &IpVersion::V6)?; + result += &build_filter_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, &IpVersion::V4)?; + result += &build_filter_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, &IpVersion::V6)?; } _ => { - result += &build_filter_rule_for_family(filter, &filter.ip_version)?; + result += &build_filter_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, ip_version)?; } } @@ -136,7 +157,20 @@ fn build_filter_rule(filter: &FilterRule) -> Result { } /// 为特定IP family构建过滤规则 -fn build_filter_rule_for_family(filter: &FilterRule, ip_version: &IpVersion) -> Result { +#[allow(clippy::too_many_arguments)] +fn build_filter_rule_for_family( + cell: &NftCell, + chain: &Chain, + src_ip: &Option, + dst_ip: &Option, + src_port: &Option, + src_port_end: &Option, + dst_port: &Option, + dst_port_end: &Option, + protocol: &Protocol, + comment: &Option, + ip_version: &IpVersion, +) -> Result { let (family, ip_prefix) = match ip_version { IpVersion::V4 => ("ip", "ip"), IpVersion::V6 => ("ip6", "ip6"), @@ -148,7 +182,7 @@ fn build_filter_rule_for_family(filter: &FilterRule, ip_version: &IpVersion) -> } }; - let chain_name = match filter.chain { + let chain_name = match chain { Chain::Input => "INPUT", Chain::Forward => "FORWARD", }; @@ -156,44 +190,44 @@ fn build_filter_rule_for_family(filter: &FilterRule, ip_version: &IpVersion) -> let mut conditions = Vec::new(); // 添加协议条件 - if filter.protocol != Protocol::All || filter.src_port.is_some() || filter.dst_port.is_some() { - let proto = filter.protocol.nft_proto(); + if *protocol != Protocol::All || src_port.is_some() || dst_port.is_some() { + let proto = protocol.nft_proto(); conditions.push(proto.to_string()); } // 添加源IP条件 - if let Some(ref src_ip) = filter.src_ip { - conditions.push(format!("{} saddr {}", ip_prefix, src_ip)); + if let Some(ip) = src_ip { + conditions.push(format!("{} saddr {}", ip_prefix, ip)); } // 添加目标IP条件 - if let Some(ref dst_ip) = filter.dst_ip { - conditions.push(format!("{} daddr {}", ip_prefix, dst_ip)); + if let Some(ip) = dst_ip { + conditions.push(format!("{} daddr {}", ip_prefix, ip)); } // 添加源端口条件 - if let Some(src_port) = filter.src_port { - if let Some(end) = filter.src_port_end { - conditions.push(format!("sport {}-{}", src_port, end)); + if let Some(port) = src_port { + if let Some(end) = src_port_end { + conditions.push(format!("sport {}-{}", port, end)); } else { - conditions.push(format!("sport {}", src_port)); + conditions.push(format!("sport {}", port)); } } // 添加目标端口条件 - if let Some(dst_port) = filter.dst_port { - if let Some(end) = filter.dst_port_end { - conditions.push(format!("dport {}-{}", dst_port, end)); + if let Some(port) = dst_port { + if let Some(end) = dst_port_end { + conditions.push(format!("dport {}-{}", port, end)); } else { - conditions.push(format!("dport {}", dst_port)); + conditions.push(format!("dport {}", port)); } } let conditions_str = conditions.join(" "); - let comment_str = if let Some(ref comment) = filter.comment { - format!(" comment \"{}\"", comment) + let comment_str = if let Some(cmt) = comment { + format!(" comment \"{}\"", cmt) } else { - format!(" comment \"{}\"", filter) + format!(" comment \"{}\"", cell) }; let rule = format!( @@ -265,6 +299,10 @@ fn build_nat_rules(cell: &NftCell, dst_ip: &str, ip_version: &IpVersion) -> Resu io::ErrorKind::InvalidData, "Redirect cell should be built via build_redirect_rules", )), + NftCell::Filter { .. } => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Filter cell should be built via build_filter_rule", + )), } } @@ -330,7 +368,7 @@ fn build_redirect_rule(cell: &NftCell, ip_version: &IpVersion) -> Result Option { let line = line.trim(); @@ -339,17 +377,7 @@ fn parse_legacy_line(line: &str) -> Option { return Some(RuntimeCell::Comment(line.to_string())); } - // 先尝试解析为FilterRule - match FilterRule::try_from(line) { - Ok(filter) => return Some(RuntimeCell::Filter(filter)), - Err(ParseError::Skip) => {} // 不是Filter规则,继续尝试其他类型 - Err(ParseError::InvalidFormat(msg)) => { - log::warn!("跳过无效的过滤规则: {}", msg); - return None; - } - } - - // 使用 nat-common 的 TryFrom 解析NAT规则 + // 使用 nat-common 的 TryFrom 解析(包括NAT规则和Filter规则) match NftCell::try_from(line) { Ok(cell) => Some(RuntimeCell::Rule(cell)), Err(ParseError::Skip) => None, @@ -405,13 +433,14 @@ pub fn read_toml_config(toml_path: &str) -> Result, io::Error> let mut cells = Vec::new(); - // 处理NAT规则 + // 处理所有规则(包括NAT和Filter) for rule in config.rules { // 如果有注释,先添加注释 let comment = match &rule { NftCell::Single { comment, .. } => comment.clone(), NftCell::Range { comment, .. } => comment.clone(), NftCell::Redirect { comment, .. } => comment.clone(), + NftCell::Filter { comment, .. } => comment.clone(), }; if let Some(comment_text) = comment { @@ -421,15 +450,6 @@ pub fn read_toml_config(toml_path: &str) -> Result, io::Error> cells.push(RuntimeCell::Rule(rule)); } - // 处理过滤规则 - for filter in config.filters { - if let Some(ref comment_text) = filter.comment { - cells.push(RuntimeCell::Comment(format!("# {comment_text}"))); - } - - cells.push(RuntimeCell::Filter(filter)); - } - Ok(cells) } @@ -469,9 +489,7 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { ip_version: IpVersion::All, comment: Some("端口范围重定向到本机示例".to_string()), }, - ], - filters: vec![ - FilterRule { + NftCell::Filter { chain: Chain::Input, src_ip: Some("180.213.132.211".to_string()), dst_ip: None, @@ -483,7 +501,7 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { ip_version: IpVersion::V4, comment: Some("阻止特定IPv4地址".to_string()), }, - FilterRule { + NftCell::Filter { chain: Chain::Input, src_ip: Some("240e:328:1301::/48".to_string()), dst_ip: None, @@ -495,7 +513,7 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { ip_version: IpVersion::V6, comment: Some("阻止IPv6网段".to_string()), }, - FilterRule { + NftCell::Filter { chain: Chain::Input, src_ip: None, dst_ip: None, diff --git a/nat-common/src/lib.rs b/nat-common/src/lib.rs index 8ca02ef..c9b6d91 100644 --- a/nat-common/src/lib.rs +++ b/nat-common/src/lib.rs @@ -229,64 +229,6 @@ impl<'de> Deserialize<'de> for Protocol { pub struct TomlConfig { #[serde(default)] pub rules: Vec, - #[serde(default)] - pub filters: Vec, -} - -// Filter规则定义 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FilterRule { - #[serde(default)] - pub chain: Chain, - #[serde(skip_serializing_if = "Option::is_none")] - pub src_ip: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dst_ip: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub src_port: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub src_port_end: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dst_port: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dst_port_end: Option, - #[serde(default)] - pub protocol: Protocol, - #[serde(default)] - pub ip_version: IpVersion, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub comment: Option, -} - -impl Display for FilterRule { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut parts = vec![format!("FILTER,{}", self.chain)]; - - if let Some(ref ip) = self.src_ip { - parts.push(format!("src_ip={}", ip)); - } - if let Some(ref ip) = self.dst_ip { - parts.push(format!("dst_ip={}", ip)); - } - if let Some(port) = self.src_port { - if let Some(end) = self.src_port_end { - parts.push(format!("src_port={}-{}", port, end)); - } else { - parts.push(format!("src_port={}", port)); - } - } - if let Some(port) = self.dst_port { - if let Some(end) = self.dst_port_end { - parts.push(format!("dst_port={}-{}", port, end)); - } else { - parts.push(format!("dst_port={}", port)); - } - } - parts.push(format!("{}", self.protocol)); - parts.push(format!("{}", self.ip_version)); - - write!(f, "{}", parts.join(",")) - } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -337,6 +279,29 @@ pub enum NftCell { #[serde(default, skip_serializing_if = "Option::is_none")] comment: Option, }, + #[serde(rename = "filter")] + Filter { + #[serde(default)] + chain: Chain, + #[serde(skip_serializing_if = "Option::is_none")] + src_ip: Option, + #[serde(skip_serializing_if = "Option::is_none")] + dst_ip: Option, + #[serde(skip_serializing_if = "Option::is_none")] + src_port: Option, + #[serde(skip_serializing_if = "Option::is_none")] + src_port_end: Option, + #[serde(skip_serializing_if = "Option::is_none")] + dst_port: Option, + #[serde(skip_serializing_if = "Option::is_none")] + dst_port_end: Option, + #[serde(default)] + protocol: Protocol, + #[serde(default)] + ip_version: IpVersion, + #[serde(default, skip_serializing_if = "Option::is_none")] + comment: Option, + }, } impl Display for NftCell { @@ -378,6 +343,45 @@ impl Display for NftCell { write!(f, "REDIRECT,{src_port},{dst_port},{protocol},{ip_version}") } } + NftCell::Filter { + chain, + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + protocol, + ip_version, + .. + } => { + let mut parts = vec![format!("FILTER,{}", chain)]; + + if let Some(ip) = src_ip { + parts.push(format!("src_ip={}", ip)); + } + if let Some(ip) = dst_ip { + parts.push(format!("dst_ip={}", ip)); + } + if let Some(port) = src_port { + if let Some(end) = src_port_end { + parts.push(format!("src_port={}-{}", port, end)); + } else { + parts.push(format!("src_port={}", port)); + } + } + if let Some(port) = dst_port { + if let Some(end) = dst_port_end { + parts.push(format!("dst_port={}-{}", port, end)); + } else { + parts.push(format!("dst_port={}", port)); + } + } + parts.push(format!("{}", protocol)); + parts.push(format!("{}", ip_version)); + + write!(f, "{}", parts.join(",")) + } } } } @@ -389,10 +393,6 @@ impl TomlConfig { rule.validate() .map_err(|e| format!("规则 {} 验证失败: {}", idx + 1, e))?; } - for (idx, filter) in self.filters.iter().enumerate() { - filter.validate() - .map_err(|e| format!("过滤规则 {} 验证失败: {}", idx + 1, e))?; - } Ok(()) } @@ -426,12 +426,99 @@ impl TryFrom<&str> for NftCell { let cells: Vec<&str> = line.split(',').collect(); let rule_type = cells.first().map(|s| s.trim()).unwrap_or(""); - // 如果是FILTER类型,返回Skip让FilterRule处理 + // 处理FILTER类型 if rule_type == "FILTER" { - return Err(ParseError::Skip); + if cells.len() < 3 { + return Err(ParseError::InvalidFormat(format!( + "无效的过滤规则: {line}, FILTER类型至少需要3个字段" + ))); + } + + let chain: Chain = cells[1].trim().into(); + + let mut src_ip: Option = None; + let mut dst_ip: Option = None; + let mut src_port: Option = None; + let mut src_port_end: Option = None; + let mut dst_port: Option = None; + let mut dst_port_end: Option = None; + let mut protocol = Protocol::All; + let mut ip_version = IpVersion::All; + + // 解析key=value对和其他参数 + for i in 2..cells.len() { + let cell = cells[i].trim(); + + // 检查是否是协议 + if cell == "tcp" || cell == "udp" || cell == "all" { + protocol = cell.into(); + continue; + } + + // 检查是否是IP版本 + if cell == "ipv4" || cell == "ipv6" { + ip_version = cell.into(); + continue; + } + + // 解析key=value + if let Some(eq_pos) = cell.find('=') { + let key = &cell[..eq_pos]; + let value = &cell[eq_pos + 1..]; + + match key { + "src_ip" => src_ip = Some(value.to_string()), + "dst_ip" => dst_ip = Some(value.to_string()), + "src_port" => { + if value.contains('-') { + let parts: Vec<&str> = value.split('-').collect(); + if parts.len() != 2 { + return Err(ParseError::InvalidFormat(format!( + "无效的端口范围格式: {value}" + ))); + } + src_port = Some(parts[0].parse::()?); + src_port_end = Some(parts[1].parse::()?); + } else { + src_port = Some(value.parse::()?); + } + } + "dst_port" => { + if value.contains('-') { + let parts: Vec<&str> = value.split('-').collect(); + if parts.len() != 2 { + return Err(ParseError::InvalidFormat(format!( + "无效的端口范围格式: {value}" + ))); + } + dst_port = Some(parts[0].parse::()?); + dst_port_end = Some(parts[1].parse::()?); + } else { + dst_port = Some(value.parse::()?); + } + } + _ => return Err(ParseError::InvalidFormat(format!( + "未知的过滤参数: {key}" + ))), + } + } + } + + return Ok(NftCell::Filter { + chain, + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + protocol, + ip_version, + comment: None, + }); } - // 验证字段数量 + // 验证字段数量(对于非FILTER类型) match rule_type { "REDIRECT" => { if cells.len() < 3 || cells.len() > 5 { @@ -544,119 +631,6 @@ impl TryFrom<&str> for NftCell { } } -impl TryFrom<&str> for FilterRule { - type Error = ParseError; - - /// 从legacy格式行解析FilterRule - /// 格式: FILTER,chain,key=value,key=value,...,protocol,ip_version - /// 示例: FILTER,input,src_ip=192.168.1.1,tcp,ipv4 - /// 示例: FILTER,forward,dst_port=80-443,src_ip=10.0.0.0/24,tcp,all - fn try_from(line: &str) -> Result { - let line = line.trim(); - - // 处理注释和空行 - if line.is_empty() || line.starts_with('#') { - return Err(ParseError::Skip); - } - - let cells: Vec<&str> = line.split(',').collect(); - let rule_type = cells.first().map(|s| s.trim()).unwrap_or(""); - - if rule_type != "FILTER" { - return Err(ParseError::Skip); - } - - if cells.len() < 3 { - return Err(ParseError::InvalidFormat(format!( - "无效的过滤规则: {line}, FILTER类型至少需要3个字段" - ))); - } - - let chain: Chain = cells[1].trim().into(); - - let mut src_ip: Option = None; - let mut dst_ip: Option = None; - let mut src_port: Option = None; - let mut src_port_end: Option = None; - let mut dst_port: Option = None; - let mut dst_port_end: Option = None; - let mut protocol = Protocol::All; - let mut ip_version = IpVersion::All; - - // 解析key=value对和其他参数 - for i in 2..cells.len() { - let cell = cells[i].trim(); - - // 检查是否是协议 - if cell == "tcp" || cell == "udp" || cell == "all" { - protocol = cell.into(); - continue; - } - - // 检查是否是IP版本 - if cell == "ipv4" || cell == "ipv6" { - ip_version = cell.into(); - continue; - } - - // 解析key=value - if let Some(eq_pos) = cell.find('=') { - let key = &cell[..eq_pos]; - let value = &cell[eq_pos + 1..]; - - match key { - "src_ip" => src_ip = Some(value.to_string()), - "dst_ip" => dst_ip = Some(value.to_string()), - "src_port" => { - if value.contains('-') { - let parts: Vec<&str> = value.split('-').collect(); - if parts.len() != 2 { - return Err(ParseError::InvalidFormat(format!( - "无效的端口范围格式: {value}" - ))); - } - src_port = Some(parts[0].parse::()?); - src_port_end = Some(parts[1].parse::()?); - } else { - src_port = Some(value.parse::()?); - } - } - "dst_port" => { - if value.contains('-') { - let parts: Vec<&str> = value.split('-').collect(); - if parts.len() != 2 { - return Err(ParseError::InvalidFormat(format!( - "无效的端口范围格式: {value}" - ))); - } - dst_port = Some(parts[0].parse::()?); - dst_port_end = Some(parts[1].parse::()?); - } else { - dst_port = Some(value.parse::()?); - } - } - _ => return Err(ParseError::InvalidFormat(format!( - "未知的过滤参数: {key}" - ))), - } - } - } - - Ok(FilterRule { - chain, - src_ip, - dst_ip, - src_port, - src_port_end, - dst_port, - dst_port_end, - protocol, - ip_version, - comment: None, - }) - } -} - impl NftCell { /// 验证单个规则是否合法 pub fn validate(&self) -> Result<(), String> { @@ -706,56 +680,58 @@ impl NftCell { validate_port(*src_port)?; validate_port(*dst_port)?; } - } - Ok(()) - } -} - -impl FilterRule { - /// 验证过滤规则是否合法 - pub fn validate(&self) -> Result<(), String> { - // 至少需要指定一个过滤条件 - if self.src_ip.is_none() - && self.dst_ip.is_none() - && self.src_port.is_none() - && self.dst_port.is_none() { - return Err("至少需要指定一个过滤条件(源IP、目标IP、源端口或目标端口)".to_string()); - } - - // 验证端口范围 - if let Some(port) = self.src_port { - validate_port(port)?; - if let Some(end) = self.src_port_end { - validate_port(end)?; - if port >= end { - return Err(format!("源端口起始 {} 必须小于结束端口 {}", port, end)); + NftCell::Filter { + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + .. + } => { + // 至少需要指定一个过滤条件 + if src_ip.is_none() + && dst_ip.is_none() + && src_port.is_none() + && dst_port.is_none() { + return Err("至少需要指定一个过滤条件(源IP、目标IP、源端口或目标端口)".to_string()); } - } - } - - if let Some(port) = self.dst_port { - validate_port(port)?; - if let Some(end) = self.dst_port_end { - validate_port(end)?; - if port >= end { - return Err(format!("目标端口起始 {} 必须小于结束端口 {}", port, end)); + + // 验证端口范围 + if let Some(port) = src_port { + validate_port(*port)?; + if let Some(end) = src_port_end { + validate_port(*end)?; + if port >= end { + return Err(format!("源端口起始 {} 必须小于结束端口 {}", port, end)); + } + } + } + + if let Some(port) = dst_port { + validate_port(*port)?; + if let Some(end) = dst_port_end { + validate_port(*end)?; + if port >= end { + return Err(format!("目标端口起始 {} 必须小于结束端口 {}", port, end)); + } + } + } + + // 验证IP地址格式(基础验证) + if let Some(ip) = src_ip { + if ip.trim().is_empty() { + return Err("源IP不能为空".to_string()); + } + } + + if let Some(ip) = dst_ip { + if ip.trim().is_empty() { + return Err("目标IP不能为空".to_string()); + } } } } - - // 验证IP地址格式(基础验证) - if let Some(ref ip) = self.src_ip { - if ip.trim().is_empty() { - return Err("源IP不能为空".to_string()); - } - } - - if let Some(ref ip) = self.dst_ip { - if ip.trim().is_empty() { - return Err("目标IP不能为空".to_string()); - } - } - Ok(()) } } diff --git a/nat-console/src/config.rs b/nat-console/src/config.rs index 91776dc..d9fb7a7 100644 --- a/nat-console/src/config.rs +++ b/nat-console/src/config.rs @@ -170,6 +170,7 @@ impl Display for ConfigFormat { pub fn get_nftables_rules() -> Result { use std::process::Command; + // Get IPv4 NAT rules let output = Command::new("/usr/sbin/nft") .arg("list") .arg("table") @@ -177,8 +178,9 @@ pub fn get_nftables_rules() -> Result { .arg("self-nat") .output()?; - let ipv4_rules = String::from_utf8_lossy(&output.stdout).to_string(); + let ipv4_nat_rules = String::from_utf8_lossy(&output.stdout).to_string(); + // Get IPv6 NAT rules let output6 = Command::new("/usr/sbin/nft") .arg("list") .arg("table") @@ -186,13 +188,42 @@ pub fn get_nftables_rules() -> Result { .arg("self-nat") .output(); - let ipv6_rules = match output6 { + let ipv6_nat_rules = match output6 { Ok(out) => String::from_utf8_lossy(&out.stdout).to_string(), - Err(_) => "# IPv6 table not found or not supported".to_string(), + Err(_) => "# IPv6 NAT table not found or not supported".to_string(), + }; + + // Get IPv4 Filter rules + let filter_output = Command::new("/usr/sbin/nft") + .arg("list") + .arg("table") + .arg("ip") + .arg("self-filter") + .output(); + + let ipv4_filter_rules = match filter_output { + Ok(out) => String::from_utf8_lossy(&out.stdout).to_string(), + Err(_) => "# IPv4 filter table not found".to_string(), + }; + + // Get IPv6 Filter rules + let filter_output6 = Command::new("/usr/sbin/nft") + .arg("list") + .arg("table") + .arg("ip6") + .arg("self-filter") + .output(); + + let ipv6_filter_rules = match filter_output6 { + Ok(out) => String::from_utf8_lossy(&out.stdout).to_string(), + Err(_) => "# IPv6 filter table not found".to_string(), }; Ok(format!( - "# IPv4 NAT Rules (table ip self-nat)\n{}\n\n# IPv6 NAT Rules (table ip6 self-nat)\n{}", - ipv4_rules, ipv6_rules + "# IPv4 NAT Rules (table ip self-nat)\n{}\n\n\ + # IPv6 NAT Rules (table ip6 self-nat)\n{}\n\n\ + # IPv4 Filter Rules (table ip self-filter)\n{}\n\n\ + # IPv6 Filter Rules (table ip6 self-filter)\n{}", + ipv4_nat_rules, ipv6_nat_rules, ipv4_filter_rules, ipv6_filter_rules )) } diff --git a/setup.sh b/setup.sh index fdb457a..2f0d7e8 100644 --- a/setup.sh +++ b/setup.sh @@ -60,7 +60,7 @@ Wants=network-online.target WorkingDirectory=/opt/nat EnvironmentFile=/opt/nat/env ExecStart=$EXEC_START -ExecStop=/bin/bash -c 'nft add table ip self-nat; nft delete table ip self-nat; nft add table ip6 self-nat; nft delete table ip6 self-nat' +ExecStop=/bin/bash -c 'nft add table ip self-nat; nft delete table ip self-nat; nft add table ip6 self-nat; nft delete table ip6 self-nat; nft add table ip self-filter; nft delete table ip self-filter; nft add table ip6 self-filter; nft delete table ip6 self-filter' LimitNOFILE=100000 Restart=always RestartSec=60 From 6da33f57946d2956f293fb934b563e3fecb85f10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 09:48:51 +0000 Subject: [PATCH 4/8] Complete filter drop capability implementation with README updates Co-authored-by: arloor <21768987+arloor@users.noreply.github.com> --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 0f294e3..2043530 100644 --- a/README.md +++ b/README.md @@ -259,11 +259,15 @@ comment = "双栈 Web 服务" - `RANGE,起始端口,结束端口,目标地址[,协议][,IP版本]` - 端口段转发 - `REDIRECT,源端口,目标端口[,协议][,IP版本]` - 重定向到本机端口 - `REDIRECT,起始端口-结束端口,目标端口[,协议][,IP版本]` - 端口段重定向 +- `FILTER,链类型,过滤条件[,协议][,IP版本]` - 防火墙过滤规则 **参数说明**: - 协议可选值:`tcp`、`udp`、`all`(默认为 `all`) - IP 版本可选值:`ipv4`、`ipv6`、`all`(默认为 `all`) +- 链类型可选值:`input`、`forward` +- 过滤条件格式:`key=value`,支持 `src_ip`、`dst_ip`、`src_port`、`dst_port` +- 端口范围格式:`port=start-end`(如 `dst_port=1000-2000`) - 以 `#` 开头的行为注释 **配置示例**: @@ -304,6 +308,26 @@ SINGLE,9001,9090,ipv6.example.com,all,ipv6 # 双栈支持(根据客户端自动选择) SINGLE,10080,80,dual-stack.example.com,tcp,all +# ============ 防火墙过滤规则 (Filter Drop) ============ + +# 阻止特定 IPv4 地址访问 +FILTER,input,src_ip=180.213.132.211,all,ipv4 + +# 阻止 IPv6 网段访问 +FILTER,input,src_ip=240e:328:1301::/48,all,ipv6 + +# 阻止 SSH 端口访问(所有IP) +FILTER,input,dst_port=22,tcp,all + +# 阻止端口范围转发 +FILTER,forward,dst_port=1000-2000,tcp,ipv4 + +# 组合过滤:阻止特定网段访问MySQL +FILTER,input,src_ip=192.168.1.0/24,dst_port=3306,tcp,ipv4 + +# 阻止特定源端口 +FILTER,forward,src_port=5000-6000,tcp,all + # 禁用的规则(以 # 开头) # SINGLE,3000,3000,disabled.example.com ``` From 2d9fcbb7816815d81c30f4b5702487c5cbe450cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:01:32 +0000 Subject: [PATCH 5/8] Add IP version validation using ipnetwork library for comprehensive IP address checking Co-authored-by: arloor <21768987+arloor@users.noreply.github.com> --- Cargo.lock | 10 ++ Cargo.toml | 1 + nat-common/Cargo.toml | 1 + nat-common/src/lib.rs | 240 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 251 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index bd38231..e928d88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -815,6 +815,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -994,6 +1003,7 @@ dependencies = [ "chrono", "clap", "env_logger", + "ipnetwork", "serde", "serde_json", "toml", diff --git a/Cargo.toml b/Cargo.toml index 65ada7e..a4cd972 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1.0" chrono = "0.4" clap = { version = "4.5", features = ["derive"] } toml = "0.9" +ipnetwork = "0.20" # WebUI 相关依赖 axum = "0.8" diff --git a/nat-common/Cargo.toml b/nat-common/Cargo.toml index 690e53c..56fb5f3 100644 --- a/nat-common/Cargo.toml +++ b/nat-common/Cargo.toml @@ -11,3 +11,4 @@ toml.workspace = true env_logger.workspace = true chrono.workspace = true clap.workspace = true +ipnetwork.workspace = true diff --git a/nat-common/src/lib.rs b/nat-common/src/lib.rs index c9b6d91..6b9f83a 100644 --- a/nat-common/src/lib.rs +++ b/nat-common/src/lib.rs @@ -2,6 +2,7 @@ use clap::Parser; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::Display; use std::num::ParseIntError; +use std::str::FromStr; pub mod logger; @@ -687,6 +688,7 @@ impl NftCell { src_port_end, dst_port, dst_port_end, + ip_version, .. } => { // 至少需要指定一个过滤条件 @@ -718,17 +720,19 @@ impl NftCell { } } - // 验证IP地址格式(基础验证) + // 验证IP地址格式和IP版本匹配 if let Some(ip) = src_ip { if ip.trim().is_empty() { return Err("源IP不能为空".to_string()); } + validate_ip_version_match(ip, ip_version, "源IP")?; } if let Some(ip) = dst_ip { if ip.trim().is_empty() { return Err("目标IP不能为空".to_string()); } + validate_ip_version_match(ip, ip_version, "目标IP")?; } } } @@ -743,6 +747,39 @@ fn validate_port(port: u16) -> Result<(), String> { Ok(()) } +/// 验证IP地址与IP版本是否匹配 +fn validate_ip_version_match(ip: &str, ip_version: &IpVersion, field_name: &str) -> Result<(), String> { + // 尝试解析为 IpNetwork(支持 CIDR 表示法) + if let Ok(network) = ipnetwork::IpNetwork::from_str(ip) { + let is_ipv6 = network.is_ipv6(); + + match ip_version { + IpVersion::V4 => { + if is_ipv6 { + return Err(format!( + "{}地址 '{}' 是IPv6格式,但IP版本设置为ipv4", + field_name, ip + )); + } + } + IpVersion::V6 => { + if !is_ipv6 { + return Err(format!( + "{}地址 '{}' 是IPv4格式,但IP版本设置为ipv6", + field_name, ip + )); + } + } + IpVersion::All => { + // All版本允许任何IP格式 + } + } + Ok(()) + } else { + Err(format!("{}地址 '{}' 格式无效", field_name, ip)) + } +} + /// 验证legacy格式配置内容 /// 返回第一个遇到的错误,跳过注释和空行 pub fn validate_legacy_config(content: &str) -> Result<(), String> { @@ -954,4 +991,205 @@ ip_version = "all" let result = validate_legacy_config(content); assert!(result.is_err()); } + + #[test] + fn test_filter_ipv4_with_ipv4_address() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("192.168.1.1".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V4, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_filter_ipv6_with_ipv6_address() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("2001:db8::1".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V6, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_filter_ipv4_with_ipv6_address_fails() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("2001:db8::1".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V4, + comment: None, + }; + let result = rule.validate(); + assert!(result.is_err()); + let err_msg = result.unwrap_err(); + assert!(err_msg.contains("IPv6格式")); + assert!(err_msg.contains("ipv4")); + } + + #[test] + fn test_filter_ipv6_with_ipv4_address_fails() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: None, + dst_ip: Some("192.168.1.1".to_string()), + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V6, + comment: None, + }; + let result = rule.validate(); + assert!(result.is_err()); + let err_msg = result.unwrap_err(); + assert!(err_msg.contains("IPv4格式")); + assert!(err_msg.contains("ipv6")); + } + + #[test] + fn test_filter_all_with_ipv4_address() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("10.0.0.1".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::All, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_filter_all_with_ipv6_address() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("fe80::1".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::All, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_filter_ipv4_cidr_notation() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("192.168.1.0/24".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V4, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_filter_ipv6_cidr_notation() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("2001:db8::/32".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V6, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_filter_invalid_ip_address() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("invalid.ip.address".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V4, + comment: None, + }; + let result = rule.validate(); + assert!(result.is_err()); + let err_msg = result.unwrap_err(); + assert!(err_msg.contains("格式无效")); + } + + #[test] + fn test_filter_invalid_cidr() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("192.168.1.1/99".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V4, + comment: None, + }; + let result = rule.validate(); + assert!(result.is_err()); + let err_msg = result.unwrap_err(); + assert!(err_msg.contains("格式无效")); + } + + #[test] + fn test_filter_valid_ipv6_full() { + let rule = NftCell::Filter { + chain: Chain::Input, + src_ip: Some("2001:0db8:85a3:0000:0000:8a2e:0370:7334".to_string()), + dst_ip: None, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + ip_version: IpVersion::V6, + comment: None, + }; + assert!(rule.validate().is_ok()); + } } From b50d7be35ba86558737d3bff6085c87b8b2dea65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:11:36 +0000 Subject: [PATCH 6/8] Rename FILTER type to DROP throughout codebase Co-authored-by: arloor <21768987+arloor@users.noreply.github.com> --- README.md | 30 ++++++++++---------- nat-cli/src/config.rs | 40 +++++++++++++------------- nat-common/src/lib.rs | 66 +++++++++++++++++++++---------------------- 3 files changed, 68 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 2043530..c9b6e94 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## ✨ 核心特性 - 🔄 **动态 NAT 转发**:自动监测配置文件和目标域名 IP 变化,实时更新转发规则 -- 🛡️ **防火墙过滤**:支持 Filter Drop 功能,实现类似防火墙的黑名单过滤(INPUT/FORWARD链) +- 🛡️ **防火墙过滤**:支持 Drop 功能,实现类似防火墙的黑名单过滤(INPUT/FORWARD链) - 🌐 **IPv4/IPv6 双栈支持**:完整支持 IPv4 和 IPv6 NAT 转发和过滤 - 📝 **灵活配置**:支持传统配置文件和 TOML 格式,满足不同使用场景 - 🎯 **精准控制**:支持单端口、端口段、TCP/UDP 协议选择、IP地址和网段过滤 @@ -177,11 +177,11 @@ protocol = "tcp" ip_version = "all" comment = "批量端口重定向到本机" -# ============ 防火墙过滤规则 (Filter Drop) ============ +# ============ 防火墙过滤规则 (Drop) ============ # 6. 阻止特定 IPv4 地址访问 [[rules]] -type = "filter" +type = "drop" chain = "input" # 链类型: input 或 forward src_ip = "180.213.132.211" # 源 IP 地址 protocol = "all" # 协议: all, tcp 或 udp @@ -190,7 +190,7 @@ comment = "阻止恶意 IP 访问" # 7. 阻止 IPv6 网段访问 [[rules]] -type = "filter" +type = "drop" chain = "input" src_ip = "240e:328:1301::/48" # IPv6 网段 protocol = "all" @@ -199,7 +199,7 @@ comment = "阻止 IPv6 网段访问" # 8. 阻止特定端口(如 SSH) [[rules]] -type = "filter" +type = "drop" chain = "input" dst_port = 22 # 目标端口 protocol = "tcp" @@ -208,7 +208,7 @@ comment = "阻止 SSH 端口访问" # 9. 阻止端口范围 [[rules]] -type = "filter" +type = "drop" chain = "forward" dst_port = 1000 # 起始端口 dst_port_end = 2000 # 结束端口 @@ -218,7 +218,7 @@ comment = "阻止转发到端口范围 1000-2000" # 10. 组合过滤:特定IP访问特定端口 [[rules]] -type = "filter" +type = "drop" chain = "input" src_ip = "192.168.1.0/24" # 源 IP 网段 dst_port = 3306 # 目标端口 (MySQL) @@ -259,7 +259,7 @@ comment = "双栈 Web 服务" - `RANGE,起始端口,结束端口,目标地址[,协议][,IP版本]` - 端口段转发 - `REDIRECT,源端口,目标端口[,协议][,IP版本]` - 重定向到本机端口 - `REDIRECT,起始端口-结束端口,目标端口[,协议][,IP版本]` - 端口段重定向 -- `FILTER,链类型,过滤条件[,协议][,IP版本]` - 防火墙过滤规则 +- `DROP,链类型,过滤条件[,协议][,IP版本]` - 防火墙过滤规则 **参数说明**: @@ -308,25 +308,25 @@ SINGLE,9001,9090,ipv6.example.com,all,ipv6 # 双栈支持(根据客户端自动选择) SINGLE,10080,80,dual-stack.example.com,tcp,all -# ============ 防火墙过滤规则 (Filter Drop) ============ +# ============ 防火墙过滤规则 (Drop) ============ # 阻止特定 IPv4 地址访问 -FILTER,input,src_ip=180.213.132.211,all,ipv4 +DROP,input,src_ip=180.213.132.211,all,ipv4 # 阻止 IPv6 网段访问 -FILTER,input,src_ip=240e:328:1301::/48,all,ipv6 +DROP,input,src_ip=240e:328:1301::/48,all,ipv6 # 阻止 SSH 端口访问(所有IP) -FILTER,input,dst_port=22,tcp,all +DROP,input,dst_port=22,tcp,all # 阻止端口范围转发 -FILTER,forward,dst_port=1000-2000,tcp,ipv4 +DROP,forward,dst_port=1000-2000,tcp,ipv4 # 组合过滤:阻止特定网段访问MySQL -FILTER,input,src_ip=192.168.1.0/24,dst_port=3306,tcp,ipv4 +DROP,input,src_ip=192.168.1.0/24,dst_port=3306,tcp,ipv4 # 阻止特定源端口 -FILTER,forward,src_port=5000-6000,tcp,all +DROP,forward,src_port=5000-6000,tcp,all # 禁用的规则(以 # 开头) # SINGLE,3000,3000,disabled.example.com diff --git a/nat-cli/src/config.rs b/nat-cli/src/config.rs index 648c8af..01e374e 100644 --- a/nat-cli/src/config.rs +++ b/nat-cli/src/config.rs @@ -50,7 +50,7 @@ pub trait NftCellBuilder { impl NftCellBuilder for NftCell { fn build(&self) -> Result { match self { - NftCell::Filter { .. } => build_filter_rule(self), + NftCell::Drop { .. } => build_drop_rule(self), _ => { let (domain, ip_version) = match &self { NftCell::Single { @@ -67,7 +67,7 @@ impl NftCellBuilder for NftCell { // Redirect doesn't need domain resolution return build_redirect_rules(self, ip_version); } - NftCell::Filter { .. } => unreachable!(), + NftCell::Drop { .. } => unreachable!(), }; // 根据配置的IP版本解析目标IP @@ -122,8 +122,8 @@ impl RuntimeCell { } /// 构建过滤规则的nftables脚本 -fn build_filter_rule(cell: &NftCell) -> Result { - let NftCell::Filter { +fn build_drop_rule(cell: &NftCell) -> Result { + let NftCell::Drop { chain, src_ip, dst_ip, @@ -137,7 +137,7 @@ fn build_filter_rule(cell: &NftCell) -> Result { } = cell else { return Err(io::Error::new( io::ErrorKind::InvalidData, - "Expected Filter cell", + "Expected Drop cell", )); }; @@ -145,11 +145,11 @@ fn build_filter_rule(cell: &NftCell) -> Result { match ip_version { IpVersion::All => { - result += &build_filter_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, &IpVersion::V4)?; - result += &build_filter_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, &IpVersion::V6)?; + result += &build_drop_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, &IpVersion::V4)?; + result += &build_drop_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, &IpVersion::V6)?; } _ => { - result += &build_filter_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, ip_version)?; + result += &build_drop_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, ip_version)?; } } @@ -158,7 +158,7 @@ fn build_filter_rule(cell: &NftCell) -> Result { /// 为特定IP family构建过滤规则 #[allow(clippy::too_many_arguments)] -fn build_filter_rule_for_family( +fn build_drop_rule_for_family( cell: &NftCell, chain: &Chain, src_ip: &Option, @@ -299,9 +299,9 @@ fn build_nat_rules(cell: &NftCell, dst_ip: &str, ip_version: &IpVersion) -> Resu io::ErrorKind::InvalidData, "Redirect cell should be built via build_redirect_rules", )), - NftCell::Filter { .. } => Err(io::Error::new( + NftCell::Drop { .. } => Err(io::Error::new( io::ErrorKind::InvalidData, - "Filter cell should be built via build_filter_rule", + "Drop cell should be built via build_drop_rule", )), } } @@ -396,13 +396,13 @@ pub(crate) fn example(conf: &str) { RANGE,1000,2000,baidu.com,tcp,ipv6\n\ REDIRECT,8000,3128,all,ipv4\n\ REDIRECT,8000-9000,3128,tcp,all\n\ - FILTER,input,src_ip=180.213.132.211,all,ipv4\n\ - FILTER,input,src_ip=240e:328:1301::/48,all,ipv6\n\ - FILTER,forward,dst_port=22,tcp,all\n\ + DROP,input,src_ip=180.213.132.211,all,ipv4\n\ + DROP,input,src_ip=240e:328:1301::/48,all,ipv6\n\ + DROP,forward,dst_port=22,tcp,all\n\ # 格式: TYPE,port(s),port/domain,protocol,ip_version\n\ - # TYPE: SINGLE, RANGE, REDIRECT 或 FILTER\n\ + # TYPE: SINGLE, RANGE, REDIRECT 或 DROP\n\ # REDIRECT格式: REDIRECT,src_port,dst_port 或 REDIRECT,src_port-src_port_end,dst_port\n\ - # FILTER格式: FILTER,chain,key=value,...,protocol,ip_version\n\ + # DROP格式: DROP,chain,key=value,...,protocol,ip_version\n\ # chain: input 或 forward\n\ # key=value: src_ip=IP, dst_ip=IP, src_port=PORT, dst_port=PORT\n\ # protocol: tcp, udp, all\n\ @@ -440,7 +440,7 @@ pub fn read_toml_config(toml_path: &str) -> Result, io::Error> NftCell::Single { comment, .. } => comment.clone(), NftCell::Range { comment, .. } => comment.clone(), NftCell::Redirect { comment, .. } => comment.clone(), - NftCell::Filter { comment, .. } => comment.clone(), + NftCell::Drop { comment, .. } => comment.clone(), }; if let Some(comment_text) = comment { @@ -489,7 +489,7 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { ip_version: IpVersion::All, comment: Some("端口范围重定向到本机示例".to_string()), }, - NftCell::Filter { + NftCell::Drop { chain: Chain::Input, src_ip: Some("180.213.132.211".to_string()), dst_ip: None, @@ -501,7 +501,7 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { ip_version: IpVersion::V4, comment: Some("阻止特定IPv4地址".to_string()), }, - NftCell::Filter { + NftCell::Drop { chain: Chain::Input, src_ip: Some("240e:328:1301::/48".to_string()), dst_ip: None, @@ -513,7 +513,7 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { ip_version: IpVersion::V6, comment: Some("阻止IPv6网段".to_string()), }, - NftCell::Filter { + NftCell::Drop { chain: Chain::Input, src_ip: None, dst_ip: None, diff --git a/nat-common/src/lib.rs b/nat-common/src/lib.rs index 6b9f83a..cab3ad6 100644 --- a/nat-common/src/lib.rs +++ b/nat-common/src/lib.rs @@ -114,7 +114,7 @@ pub enum Protocol { Udp, } -// Filter链类型枚举 +// Drop链类型枚举 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Chain { #[default] @@ -280,8 +280,8 @@ pub enum NftCell { #[serde(default, skip_serializing_if = "Option::is_none")] comment: Option, }, - #[serde(rename = "filter")] - Filter { + #[serde(rename = "drop")] + Drop { #[serde(default)] chain: Chain, #[serde(skip_serializing_if = "Option::is_none")] @@ -344,7 +344,7 @@ impl Display for NftCell { write!(f, "REDIRECT,{src_port},{dst_port},{protocol},{ip_version}") } } - NftCell::Filter { + NftCell::Drop { chain, src_ip, dst_ip, @@ -356,7 +356,7 @@ impl Display for NftCell { ip_version, .. } => { - let mut parts = vec![format!("FILTER,{}", chain)]; + let mut parts = vec![format!("DROP,{}", chain)]; if let Some(ip) = src_ip { parts.push(format!("src_ip={}", ip)); @@ -427,11 +427,11 @@ impl TryFrom<&str> for NftCell { let cells: Vec<&str> = line.split(',').collect(); let rule_type = cells.first().map(|s| s.trim()).unwrap_or(""); - // 处理FILTER类型 - if rule_type == "FILTER" { + // 处理DROP类型 + if rule_type == "DROP" { if cells.len() < 3 { return Err(ParseError::InvalidFormat(format!( - "无效的过滤规则: {line}, FILTER类型至少需要3个字段" + "无效的过滤规则: {line}, DROP类型至少需要3个字段" ))); } @@ -505,7 +505,7 @@ impl TryFrom<&str> for NftCell { } } - return Ok(NftCell::Filter { + return Ok(NftCell::Drop { chain, src_ip, dst_ip, @@ -519,7 +519,7 @@ impl TryFrom<&str> for NftCell { }); } - // 验证字段数量(对于非FILTER类型) + // 验证字段数量(对于非DROP类型) match rule_type { "REDIRECT" => { if cells.len() < 3 || cells.len() > 5 { @@ -681,7 +681,7 @@ impl NftCell { validate_port(*src_port)?; validate_port(*dst_port)?; } - NftCell::Filter { + NftCell::Drop { src_ip, dst_ip, src_port, @@ -993,8 +993,8 @@ ip_version = "all" } #[test] - fn test_filter_ipv4_with_ipv4_address() { - let rule = NftCell::Filter { + fn test_drop_ipv4_with_ipv4_address() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("192.168.1.1".to_string()), dst_ip: None, @@ -1010,8 +1010,8 @@ ip_version = "all" } #[test] - fn test_filter_ipv6_with_ipv6_address() { - let rule = NftCell::Filter { + fn test_drop_ipv6_with_ipv6_address() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("2001:db8::1".to_string()), dst_ip: None, @@ -1027,8 +1027,8 @@ ip_version = "all" } #[test] - fn test_filter_ipv4_with_ipv6_address_fails() { - let rule = NftCell::Filter { + fn test_drop_ipv4_with_ipv6_address_fails() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("2001:db8::1".to_string()), dst_ip: None, @@ -1048,8 +1048,8 @@ ip_version = "all" } #[test] - fn test_filter_ipv6_with_ipv4_address_fails() { - let rule = NftCell::Filter { + fn test_drop_ipv6_with_ipv4_address_fails() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: None, dst_ip: Some("192.168.1.1".to_string()), @@ -1069,8 +1069,8 @@ ip_version = "all" } #[test] - fn test_filter_all_with_ipv4_address() { - let rule = NftCell::Filter { + fn test_drop_all_with_ipv4_address() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("10.0.0.1".to_string()), dst_ip: None, @@ -1086,8 +1086,8 @@ ip_version = "all" } #[test] - fn test_filter_all_with_ipv6_address() { - let rule = NftCell::Filter { + fn test_drop_all_with_ipv6_address() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("fe80::1".to_string()), dst_ip: None, @@ -1103,8 +1103,8 @@ ip_version = "all" } #[test] - fn test_filter_ipv4_cidr_notation() { - let rule = NftCell::Filter { + fn test_drop_ipv4_cidr_notation() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("192.168.1.0/24".to_string()), dst_ip: None, @@ -1120,8 +1120,8 @@ ip_version = "all" } #[test] - fn test_filter_ipv6_cidr_notation() { - let rule = NftCell::Filter { + fn test_drop_ipv6_cidr_notation() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("2001:db8::/32".to_string()), dst_ip: None, @@ -1137,8 +1137,8 @@ ip_version = "all" } #[test] - fn test_filter_invalid_ip_address() { - let rule = NftCell::Filter { + fn test_drop_invalid_ip_address() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("invalid.ip.address".to_string()), dst_ip: None, @@ -1157,8 +1157,8 @@ ip_version = "all" } #[test] - fn test_filter_invalid_cidr() { - let rule = NftCell::Filter { + fn test_drop_invalid_cidr() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("192.168.1.1/99".to_string()), dst_ip: None, @@ -1177,8 +1177,8 @@ ip_version = "all" } #[test] - fn test_filter_valid_ipv6_full() { - let rule = NftCell::Filter { + fn test_drop_valid_ipv6_full() { + let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("2001:0db8:85a3:0000:0000:8a2e:0370:7334".to_string()), dst_ip: None, From fa51ec1aa43a1fcde49c9c65e561129647d23b7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:12:45 +0000 Subject: [PATCH 7/8] Update remaining comments from Filter to Drop for consistency Co-authored-by: arloor <21768987+arloor@users.noreply.github.com> --- nat-cli/src/config.rs | 2 +- nat-cli/src/main.rs | 4 ++-- nat-console/src/config.rs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nat-cli/src/config.rs b/nat-cli/src/config.rs index 01e374e..02823ba 100644 --- a/nat-cli/src/config.rs +++ b/nat-cli/src/config.rs @@ -377,7 +377,7 @@ fn parse_legacy_line(line: &str) -> Option { return Some(RuntimeCell::Comment(line.to_string())); } - // 使用 nat-common 的 TryFrom 解析(包括NAT规则和Filter规则) + // 使用 nat-common 的 TryFrom 解析(包括NAT规则和Drop规则) match NftCell::try_from(line) { Ok(cell) => Some(RuntimeCell::Rule(cell)), Err(ParseError::Skip) => None, diff --git a/nat-cli/src/main.rs b/nat-cli/src/main.rs index 87b628e..1b598b7 100644 --- a/nat-cli/src/main.rs +++ b/nat-cli/src/main.rs @@ -167,14 +167,14 @@ fn build_new_script(nat_cells: &[config::RuntimeCell]) -> Result Result { Err(_) => "# IPv6 NAT table not found or not supported".to_string(), }; - // Get IPv4 Filter rules + // Get IPv4 Drop rules let filter_output = Command::new("/usr/sbin/nft") .arg("list") .arg("table") @@ -206,7 +206,7 @@ pub fn get_nftables_rules() -> Result { Err(_) => "# IPv4 filter table not found".to_string(), }; - // Get IPv6 Filter rules + // Get IPv6 Drop rules let filter_output6 = Command::new("/usr/sbin/nft") .arg("list") .arg("table") @@ -222,8 +222,8 @@ pub fn get_nftables_rules() -> Result { Ok(format!( "# IPv4 NAT Rules (table ip self-nat)\n{}\n\n\ # IPv6 NAT Rules (table ip6 self-nat)\n{}\n\n\ - # IPv4 Filter Rules (table ip self-filter)\n{}\n\n\ - # IPv6 Filter Rules (table ip6 self-filter)\n{}", + # IPv4 Drop Rules (table ip self-filter)\n{}\n\n\ + # IPv6 Drop Rules (table ip6 self-filter)\n{}", ipv4_nat_rules, ipv6_nat_rules, ipv4_filter_rules, ipv6_filter_rules )) } From 1f431f13c0f9e776c431f1105cf09be7bbc7e70e Mon Sep 17 00:00:00 2001 From: arloor Date: Sun, 1 Feb 2026 18:45:10 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20ipnetwork=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=B9=B6=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E4=BB=A5=E6=94=AF=E6=8C=81=20IP=20=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + README.md | 23 ++++------ nat-cli/Cargo.toml | 1 + nat-cli/src/config.rs | 83 +++++++++++++++++++++++----------- nat-common/src/lib.rs | 101 +++++++++++------------------------------- 5 files changed, 93 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e928d88..67c15c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -989,6 +989,7 @@ dependencies = [ "chrono", "clap", "env_logger", + "ipnetwork", "log", "nat-common", "serde", diff --git a/README.md b/README.md index c9b6e94..4fa886a 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,6 @@ type = "drop" chain = "input" # 链类型: input 或 forward src_ip = "180.213.132.211" # 源 IP 地址 protocol = "all" # 协议: all, tcp 或 udp -ip_version = "ipv4" # IP 版本: ipv4, ipv6 或 all comment = "阻止恶意 IP 访问" # 7. 阻止 IPv6 网段访问 @@ -194,7 +193,6 @@ type = "drop" chain = "input" src_ip = "240e:328:1301::/48" # IPv6 网段 protocol = "all" -ip_version = "ipv6" comment = "阻止 IPv6 网段访问" # 8. 阻止特定端口(如 SSH) @@ -203,7 +201,6 @@ type = "drop" chain = "input" dst_port = 22 # 目标端口 protocol = "tcp" -ip_version = "all" comment = "阻止 SSH 端口访问" # 9. 阻止端口范围 @@ -213,7 +210,6 @@ chain = "forward" dst_port = 1000 # 起始端口 dst_port_end = 2000 # 结束端口 protocol = "tcp" -ip_version = "ipv4" comment = "阻止转发到端口范围 1000-2000" # 10. 组合过滤:特定IP访问特定端口 @@ -223,7 +219,6 @@ chain = "input" src_ip = "192.168.1.0/24" # 源 IP 网段 dst_port = 3306 # 目标端口 (MySQL) protocol = "tcp" -ip_version = "ipv4" comment = "阻止内网访问 MySQL" # ============ 高级场景示例 ============ @@ -259,15 +254,15 @@ comment = "双栈 Web 服务" - `RANGE,起始端口,结束端口,目标地址[,协议][,IP版本]` - 端口段转发 - `REDIRECT,源端口,目标端口[,协议][,IP版本]` - 重定向到本机端口 - `REDIRECT,起始端口-结束端口,目标端口[,协议][,IP版本]` - 端口段重定向 -- `DROP,链类型,过滤条件[,协议][,IP版本]` - 防火墙过滤规则 +- `DROP,链类型,过滤条件[,协议]` - 防火墙过滤规则 **参数说明**: - 协议可选值:`tcp`、`udp`、`all`(默认为 `all`) -- IP 版本可选值:`ipv4`、`ipv6`、`all`(默认为 `all`) - 链类型可选值:`input`、`forward` - 过滤条件格式:`key=value`,支持 `src_ip`、`dst_ip`、`src_port`、`dst_port` -- 端口范围格式:`port=start-end`(如 `dst_port=1000-2000`) +- 端口格式:支持单个端口(如 `dst_port=443`)和端口段(如 `dst_port=1000-2000`) +- ip地址格式:支持单个 IP(如`192.168.1.0`)和IP 网段(如`192.168.1.0/24`) - 以 `#` 开头的行为注释 **配置示例**: @@ -311,22 +306,22 @@ SINGLE,10080,80,dual-stack.example.com,tcp,all # ============ 防火墙过滤规则 (Drop) ============ # 阻止特定 IPv4 地址访问 -DROP,input,src_ip=180.213.132.211,all,ipv4 +DROP,input,src_ip=180.213.132.211,all # 阻止 IPv6 网段访问 -DROP,input,src_ip=240e:328:1301::/48,all,ipv6 +DROP,input,src_ip=240e:328:1301::/48,all # 阻止 SSH 端口访问(所有IP) -DROP,input,dst_port=22,tcp,all +DROP,input,dst_port=22,tcp # 阻止端口范围转发 -DROP,forward,dst_port=1000-2000,tcp,ipv4 +DROP,forward,dst_port=1000-2000,tcp # 组合过滤:阻止特定网段访问MySQL -DROP,input,src_ip=192.168.1.0/24,dst_port=3306,tcp,ipv4 +DROP,input,src_ip=192.168.1.0/24,dst_port=3306,tcp # 阻止特定源端口 -DROP,forward,src_port=5000-6000,tcp,all +DROP,forward,src_port=5000-6000,tcp # 禁用的规则(以 # 开头) # SINGLE,3000,3000,disabled.example.com diff --git a/nat-cli/Cargo.toml b/nat-cli/Cargo.toml index d9bdad5..3ee6f80 100644 --- a/nat-cli/Cargo.toml +++ b/nat-cli/Cargo.toml @@ -16,4 +16,5 @@ serde_json.workspace = true chrono.workspace = true clap.workspace = true toml.workspace = true +ipnetwork.workspace = true nat-common = { path = "../nat-common" } diff --git a/nat-cli/src/config.rs b/nat-cli/src/config.rs index 02823ba..d034a0d 100644 --- a/nat-cli/src/config.rs +++ b/nat-cli/src/config.rs @@ -1,11 +1,13 @@ #![deny(warnings)] use crate::ip; +use ipnetwork::IpNetwork; use log::info; use nat_common::{Chain, IpVersion, NftCell, ParseError, Protocol, TomlConfig}; use std::env; use std::fmt::Display; use std::fs; use std::io; +use std::str::FromStr; /// 运行时Cell,包装NftCell和Comment /// Comment仅用于运行时表示,不进入TOML配置 @@ -54,14 +56,10 @@ impl NftCellBuilder for NftCell { _ => { let (domain, ip_version) = match &self { NftCell::Single { - domain, - ip_version, - .. + domain, ip_version, .. } => (domain, ip_version), NftCell::Range { - domain, - ip_version, - .. + domain, ip_version, .. } => (domain, ip_version), NftCell::Redirect { ip_version, .. } => { // Redirect doesn't need domain resolution @@ -132,9 +130,9 @@ fn build_drop_rule(cell: &NftCell) -> Result { dst_port, dst_port_end, protocol, - ip_version, comment, - } = cell else { + } = cell + else { return Err(io::Error::new( io::ErrorKind::InvalidData, "Expected Drop cell", @@ -143,14 +141,44 @@ fn build_drop_rule(cell: &NftCell) -> Result { let mut result = String::new(); - match ip_version { - IpVersion::All => { - result += &build_drop_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, &IpVersion::V4)?; - result += &build_drop_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, &IpVersion::V6)?; - } - _ => { - result += &build_drop_rule_for_family(cell, chain, src_ip, dst_ip, src_port, src_port_end, dst_port, dst_port_end, protocol, comment, ip_version)?; + // 判断IP版本:如果指定了src_ip或dst_ip,根据其判断family + // 如果没有指定IP地址,则在v4和v6中都添加规则 + let mut ip_families = Vec::new(); + + if let Some(ip) = src_ip.as_ref().or(dst_ip.as_ref()) { + // 根据IP地址判断family + if let Ok(network) = IpNetwork::from_str(ip) { + if network.is_ipv6() { + ip_families.push(IpVersion::V6); + } else { + ip_families.push(IpVersion::V4); + } + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("无效的IP地址: {}", ip), + )); } + } else { + // 没有指定IP地址,在v4和v6中都添加规则 + ip_families.push(IpVersion::V4); + ip_families.push(IpVersion::V6); + } + + for ip_version in ip_families { + result += &build_drop_rule_for_family( + cell, + chain, + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + protocol, + comment, + &ip_version, + )?; } Ok(result) @@ -189,13 +217,7 @@ fn build_drop_rule_for_family( let mut conditions = Vec::new(); - // 添加协议条件 - if *protocol != Protocol::All || src_port.is_some() || dst_port.is_some() { - let proto = protocol.nft_proto(); - conditions.push(proto.to_string()); - } - - // 添加源IP条件 + // 添加源IP条件(IP条件应该在协议条件之前) if let Some(ip) = src_ip { conditions.push(format!("{} saddr {}", ip_prefix, ip)); } @@ -205,6 +227,12 @@ fn build_drop_rule_for_family( conditions.push(format!("{} daddr {}", ip_prefix, ip)); } + // 添加协议条件 + if *protocol != Protocol::All || src_port.is_some() || dst_port.is_some() { + let proto = protocol.nft_proto(); + conditions.push(proto.to_string()); + } + // 添加源端口条件 if let Some(port) = src_port { if let Some(end) = src_port_end { @@ -237,7 +265,11 @@ fn build_drop_rule_for_family( Ok(rule) } -fn build_nat_rules(cell: &NftCell, dst_ip: &str, ip_version: &IpVersion) -> Result { +fn build_nat_rules( + cell: &NftCell, + dst_ip: &str, + ip_version: &IpVersion, +) -> Result { let (family, env_var, localhost_addr, fmt_ip) = match ip_version { IpVersion::V4 => ("ip", "nat_local_ip", "127.0.0.1", dst_ip.to_string()), IpVersion::V6 => ("ip6", "nat_local_ipv6", "::1", format!("[{}]", dst_ip)), @@ -486,7 +518,7 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { src_port_end: Some(39999), dst_port: 45678, protocol: Protocol::Tcp, - ip_version: IpVersion::All, + ip_version: IpVersion::V4, comment: Some("端口范围重定向到本机示例".to_string()), }, NftCell::Drop { @@ -498,7 +530,6 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V4, comment: Some("阻止特定IPv4地址".to_string()), }, NftCell::Drop { @@ -510,7 +541,6 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V6, comment: Some("阻止IPv6网段".to_string()), }, NftCell::Drop { @@ -522,7 +552,6 @@ pub fn toml_example(conf: &str) -> Result<(), io::Error> { dst_port: Some(22), dst_port_end: None, protocol: Protocol::Tcp, - ip_version: IpVersion::All, comment: Some("阻止SSH端口访问".to_string()), }, ], diff --git a/nat-common/src/lib.rs b/nat-common/src/lib.rs index cab3ad6..3855a63 100644 --- a/nat-common/src/lib.rs +++ b/nat-common/src/lib.rs @@ -44,8 +44,7 @@ impl From for ParseError { } // IP版本枚举 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[derive(Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum IpVersion { V4, V6, @@ -53,7 +52,6 @@ pub enum IpVersion { All, // 优先IPv4,如果IPv4不可用则使用IPv6 } - impl Display for IpVersion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -298,8 +296,6 @@ pub enum NftCell { dst_port_end: Option, #[serde(default)] protocol: Protocol, - #[serde(default)] - ip_version: IpVersion, #[serde(default, skip_serializing_if = "Option::is_none")] comment: Option, }, @@ -353,11 +349,10 @@ impl Display for NftCell { dst_port, dst_port_end, protocol, - ip_version, .. } => { let mut parts = vec![format!("DROP,{}", chain)]; - + if let Some(ip) = src_ip { parts.push(format!("src_ip={}", ip)); } @@ -379,8 +374,7 @@ impl Display for NftCell { } } parts.push(format!("{}", protocol)); - parts.push(format!("{}", ip_version)); - + write!(f, "{}", parts.join(",")) } } @@ -436,7 +430,7 @@ impl TryFrom<&str> for NftCell { } let chain: Chain = cells[1].trim().into(); - + let mut src_ip: Option = None; let mut dst_ip: Option = None; let mut src_port: Option = None; @@ -444,29 +438,22 @@ impl TryFrom<&str> for NftCell { let mut dst_port: Option = None; let mut dst_port_end: Option = None; let mut protocol = Protocol::All; - let mut ip_version = IpVersion::All; // 解析key=value对和其他参数 for i in 2..cells.len() { let cell = cells[i].trim(); - + // 检查是否是协议 if cell == "tcp" || cell == "udp" || cell == "all" { protocol = cell.into(); continue; } - - // 检查是否是IP版本 - if cell == "ipv4" || cell == "ipv6" { - ip_version = cell.into(); - continue; - } - + // 解析key=value if let Some(eq_pos) = cell.find('=') { let key = &cell[..eq_pos]; let value = &cell[eq_pos + 1..]; - + match key { "src_ip" => src_ip = Some(value.to_string()), "dst_ip" => dst_ip = Some(value.to_string()), @@ -498,9 +485,9 @@ impl TryFrom<&str> for NftCell { dst_port = Some(value.parse::()?); } } - _ => return Err(ParseError::InvalidFormat(format!( - "未知的过滤参数: {key}" - ))), + _ => { + return Err(ParseError::InvalidFormat(format!("未知的过滤参数: {key}"))); + } } } } @@ -514,7 +501,6 @@ impl TryFrom<&str> for NftCell { dst_port, dst_port_end, protocol, - ip_version, comment: None, }); } @@ -688,17 +674,16 @@ impl NftCell { src_port_end, dst_port, dst_port_end, - ip_version, .. } => { // 至少需要指定一个过滤条件 - if src_ip.is_none() - && dst_ip.is_none() - && src_port.is_none() - && dst_port.is_none() { - return Err("至少需要指定一个过滤条件(源IP、目标IP、源端口或目标端口)".to_string()); + if src_ip.is_none() && dst_ip.is_none() && src_port.is_none() && dst_port.is_none() + { + return Err( + "至少需要指定一个过滤条件(源IP、目标IP、源端口或目标端口)".to_string() + ); } - + // 验证端口范围 if let Some(port) = src_port { validate_port(*port)?; @@ -709,7 +694,7 @@ impl NftCell { } } } - + if let Some(port) = dst_port { validate_port(*port)?; if let Some(end) = dst_port_end { @@ -719,20 +704,20 @@ impl NftCell { } } } - - // 验证IP地址格式和IP版本匹配 + + // 验证IP地址格式 if let Some(ip) = src_ip { if ip.trim().is_empty() { return Err("源IP不能为空".to_string()); } - validate_ip_version_match(ip, ip_version, "源IP")?; + validate_ip_address(ip, "源IP")?; } - + if let Some(ip) = dst_ip { if ip.trim().is_empty() { return Err("目标IP不能为空".to_string()); } - validate_ip_version_match(ip, ip_version, "目标IP")?; + validate_ip_address(ip, "目标IP")?; } } } @@ -747,33 +732,10 @@ fn validate_port(port: u16) -> Result<(), String> { Ok(()) } -/// 验证IP地址与IP版本是否匹配 -fn validate_ip_version_match(ip: &str, ip_version: &IpVersion, field_name: &str) -> Result<(), String> { +/// 验证IP地址格式 +fn validate_ip_address(ip: &str, field_name: &str) -> Result<(), String> { // 尝试解析为 IpNetwork(支持 CIDR 表示法) - if let Ok(network) = ipnetwork::IpNetwork::from_str(ip) { - let is_ipv6 = network.is_ipv6(); - - match ip_version { - IpVersion::V4 => { - if is_ipv6 { - return Err(format!( - "{}地址 '{}' 是IPv6格式,但IP版本设置为ipv4", - field_name, ip - )); - } - } - IpVersion::V6 => { - if !is_ipv6 { - return Err(format!( - "{}地址 '{}' 是IPv4格式,但IP版本设置为ipv6", - field_name, ip - )); - } - } - IpVersion::All => { - // All版本允许任何IP格式 - } - } + if ipnetwork::IpNetwork::from_str(ip).is_ok() { Ok(()) } else { Err(format!("{}地址 '{}' 格式无效", field_name, ip)) @@ -1003,14 +965,13 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V4, comment: None, }; assert!(rule.validate().is_ok()); } #[test] - fn test_drop_ipv6_with_ipv6_address() { + fn test_drop_with_ipv6_address() { let rule = NftCell::Drop { chain: Chain::Input, src_ip: Some("2001:db8::1".to_string()), @@ -1020,7 +981,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V6, comment: None, }; assert!(rule.validate().is_ok()); @@ -1037,7 +997,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V4, comment: None, }; let result = rule.validate(); @@ -1058,7 +1017,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V6, comment: None, }; let result = rule.validate(); @@ -1079,7 +1037,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::All, comment: None, }; assert!(rule.validate().is_ok()); @@ -1096,7 +1053,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::All, comment: None, }; assert!(rule.validate().is_ok()); @@ -1113,7 +1069,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V4, comment: None, }; assert!(rule.validate().is_ok()); @@ -1130,7 +1085,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V6, comment: None, }; assert!(rule.validate().is_ok()); @@ -1147,7 +1101,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V4, comment: None, }; let result = rule.validate(); @@ -1167,7 +1120,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V4, comment: None, }; let result = rule.validate(); @@ -1187,7 +1139,6 @@ ip_version = "all" dst_port: None, dst_port_end: None, protocol: Protocol::All, - ip_version: IpVersion::V6, comment: None, }; assert!(rule.validate().is_ok());