diff --git a/Cargo.lock b/Cargo.lock index bd38231..67c15c6 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" @@ -980,6 +989,7 @@ dependencies = [ "chrono", "clap", "env_logger", + "ipnetwork", "log", "nat-common", "serde", @@ -994,6 +1004,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/README.md b/README.md index d88e19d..4fa886a 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ ## ✨ 核心特性 - 🔄 **动态 NAT 转发**:自动监测配置文件和目标域名 IP 变化,实时更新转发规则 -- 🌐 **IPv4/IPv6 双栈支持**:完整支持 IPv4 和 IPv6 NAT 转发 +- 🛡️ **防火墙过滤**:支持 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,53 @@ protocol = "tcp" ip_version = "all" comment = "批量端口重定向到本机" +# ============ 防火墙过滤规则 (Drop) ============ + +# 6. 阻止特定 IPv4 地址访问 +[[rules]] +type = "drop" +chain = "input" # 链类型: input 或 forward +src_ip = "180.213.132.211" # 源 IP 地址 +protocol = "all" # 协议: all, tcp 或 udp +comment = "阻止恶意 IP 访问" + +# 7. 阻止 IPv6 网段访问 +[[rules]] +type = "drop" +chain = "input" +src_ip = "240e:328:1301::/48" # IPv6 网段 +protocol = "all" +comment = "阻止 IPv6 网段访问" + +# 8. 阻止特定端口(如 SSH) +[[rules]] +type = "drop" +chain = "input" +dst_port = 22 # 目标端口 +protocol = "tcp" +comment = "阻止 SSH 端口访问" + +# 9. 阻止端口范围 +[[rules]] +type = "drop" +chain = "forward" +dst_port = 1000 # 起始端口 +dst_port_end = 2000 # 结束端口 +protocol = "tcp" +comment = "阻止转发到端口范围 1000-2000" + +# 10. 组合过滤:特定IP访问特定端口 +[[rules]] +type = "drop" +chain = "input" +src_ip = "192.168.1.0/24" # 源 IP 网段 +dst_port = 3306 # 目标端口 (MySQL) +protocol = "tcp" +comment = "阻止内网访问 MySQL" + # ============ 高级场景示例 ============ -# 6. 强制 IPv6 转发 +# 11. 强制 IPv6 转发 [[rules]] type = "single" sport = 9001 @@ -188,7 +233,7 @@ protocol = "all" ip_version = "ipv6" # 仅使用 IPv6 进行转发 comment = "IPv6 专用服务" -# 7. 双栈支持示例 - 自动选择 IPv4/IPv6 +# 12. 双栈支持示例 - 自动选择 IPv4/IPv6 [[rules]] type = "single" sport = 10080 @@ -209,11 +254,15 @@ comment = "双栈 Web 服务" - `RANGE,起始端口,结束端口,目标地址[,协议][,IP版本]` - 端口段转发 - `REDIRECT,源端口,目标端口[,协议][,IP版本]` - 重定向到本机端口 - `REDIRECT,起始端口-结束端口,目标端口[,协议][,IP版本]` - 端口段重定向 +- `DROP,链类型,过滤条件[,协议]` - 防火墙过滤规则 **参数说明**: - 协议可选值:`tcp`、`udp`、`all`(默认为 `all`) -- IP 版本可选值:`ipv4`、`ipv6`、`all`(默认为 `all`) +- 链类型可选值:`input`、`forward` +- 过滤条件格式:`key=value`,支持 `src_ip`、`dst_ip`、`src_port`、`dst_port` +- 端口格式:支持单个端口(如 `dst_port=443`)和端口段(如 `dst_port=1000-2000`) +- ip地址格式:支持单个 IP(如`192.168.1.0`)和IP 网段(如`192.168.1.0/24`) - 以 `#` 开头的行为注释 **配置示例**: @@ -254,6 +303,26 @@ SINGLE,9001,9090,ipv6.example.com,all,ipv6 # 双栈支持(根据客户端自动选择) SINGLE,10080,80,dual-stack.example.com,tcp,all +# ============ 防火墙过滤规则 (Drop) ============ + +# 阻止特定 IPv4 地址访问 +DROP,input,src_ip=180.213.132.211,all + +# 阻止 IPv6 网段访问 +DROP,input,src_ip=240e:328:1301::/48,all + +# 阻止 SSH 端口访问(所有IP) +DROP,input,dst_port=22,tcp + +# 阻止端口范围转发 +DROP,forward,dst_port=1000-2000,tcp + +# 组合过滤:阻止特定网段访问MySQL +DROP,input,src_ip=192.168.1.0/24,dst_port=3306,tcp + +# 阻止特定源端口 +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 0e054c4..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::{IpVersion, NftCell, ParseError, Protocol, TomlConfig}; +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配置 @@ -49,60 +51,62 @@ 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::Drop { .. } => build_drop_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::Drop { .. } => 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) } } @@ -115,7 +119,157 @@ impl RuntimeCell { } } -fn build_nat_rules(cell: &NftCell, dst_ip: &str, ip_version: &IpVersion) -> Result { +/// 构建过滤规则的nftables脚本 +fn build_drop_rule(cell: &NftCell) -> Result { + let NftCell::Drop { + chain, + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + protocol, + comment, + } = cell + else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Expected Drop cell", + )); + }; + + let mut result = String::new(); + + // 判断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) +} + +/// 为特定IP family构建过滤规则 +#[allow(clippy::too_many_arguments)] +fn build_drop_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"), + IpVersion::All => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "IpVersion::All should be handled at caller level", + )); + } + }; + + let chain_name = match chain { + Chain::Input => "INPUT", + Chain::Forward => "FORWARD", + }; + + let mut conditions = Vec::new(); + + // 添加源IP条件(IP条件应该在协议条件之前) + if let Some(ip) = src_ip { + conditions.push(format!("{} saddr {}", ip_prefix, ip)); + } + + // 添加目标IP条件 + if let Some(ip) = dst_ip { + 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 { + conditions.push(format!("sport {}-{}", port, end)); + } else { + conditions.push(format!("sport {}", port)); + } + } + + // 添加目标端口条件 + if let Some(port) = dst_port { + if let Some(end) = dst_port_end { + conditions.push(format!("dport {}-{}", port, end)); + } else { + conditions.push(format!("dport {}", port)); + } + } + + let conditions_str = conditions.join(" "); + let comment_str = if let Some(cmt) = comment { + format!(" comment \"{}\"", cmt) + } else { + format!(" comment \"{}\"", cell) + }; + + 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()), IpVersion::V6 => ("ip6", "nat_local_ipv6", "::1", format!("[{}]", dst_ip)), @@ -177,6 +331,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::Drop { .. } => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Drop cell should be built via build_drop_rule", + )), } } @@ -251,7 +409,7 @@ fn parse_legacy_line(line: &str) -> Option { return Some(RuntimeCell::Comment(line.to_string())); } - // 使用 nat-common 的 TryFrom 解析 + // 使用 nat-common 的 TryFrom 解析(包括NAT规则和Drop规则) match NftCell::try_from(line) { Ok(cell) => Some(RuntimeCell::Rule(cell)), Err(ParseError::Skip) => None, @@ -269,12 +427,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\ + 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\n\ + # TYPE: SINGLE, RANGE, REDIRECT 或 DROP\n\ # REDIRECT格式: REDIRECT,src_port,dst_port 或 REDIRECT,src_port-src_port_end,dst_port\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\ - # ip_version: ipv4, ipv6, both" + # ip_version: ipv4, ipv6, all" ) } @@ -301,12 +465,14 @@ pub fn read_toml_config(toml_path: &str) -> Result, io::Error> let mut cells = Vec::new(); + // 处理所有规则(包括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::Drop { comment, .. } => comment.clone(), }; if let Some(comment_text) = comment { @@ -352,9 +518,42 @@ 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 { + 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, + comment: Some("阻止特定IPv4地址".to_string()), + }, + NftCell::Drop { + 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, + comment: Some("阻止IPv6网段".to_string()), + }, + NftCell::Drop { + 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, + comment: Some("阻止SSH端口访问".to_string()), + }, ], }; diff --git a/nat-cli/src/main.rs b/nat-cli/src/main.rs index bf10cce..1b598b7 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 for ParseError { } // IP版本枚举 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[derive(Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum IpVersion { V4, V6, @@ -52,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 { @@ -113,6 +112,62 @@ pub enum Protocol { Udp, } +// Drop链类型枚举 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Chain { + #[default] + Input, + Forward, +} + +impl Display for Chain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> 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,6 +226,7 @@ impl<'de> Deserialize<'de> for Protocol { // TOML配置结构定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TomlConfig { + #[serde(default)] pub rules: Vec, } @@ -222,6 +278,27 @@ pub enum NftCell { #[serde(default, skip_serializing_if = "Option::is_none")] comment: Option, }, + #[serde(rename = "drop")] + Drop { + #[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, skip_serializing_if = "Option::is_none")] + comment: Option, + }, } impl Display for NftCell { @@ -263,6 +340,43 @@ impl Display for NftCell { write!(f, "REDIRECT,{src_port},{dst_port},{protocol},{ip_version}") } } + NftCell::Drop { + chain, + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + protocol, + .. + } => { + let mut parts = vec![format!("DROP,{}", 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)); + + write!(f, "{}", parts.join(",")) + } } } } @@ -307,7 +421,91 @@ impl TryFrom<&str> for NftCell { let cells: Vec<&str> = line.split(',').collect(); let rule_type = cells.first().map(|s| s.trim()).unwrap_or(""); - // 验证字段数量 + // 处理DROP类型 + if rule_type == "DROP" { + if cells.len() < 3 { + return Err(ParseError::InvalidFormat(format!( + "无效的过滤规则: {line}, DROP类型至少需要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; + + // 解析key=value对和其他参数 + for i in 2..cells.len() { + let cell = cells[i].trim(); + + // 检查是否是协议 + if cell == "tcp" || cell == "udp" || cell == "all" { + protocol = 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::Drop { + chain, + src_ip, + dst_ip, + src_port, + src_port_end, + dst_port, + dst_port_end, + protocol, + comment: None, + }); + } + + // 验证字段数量(对于非DROP类型) match rule_type { "REDIRECT" => { if cells.len() < 3 || cells.len() > 5 { @@ -469,6 +667,59 @@ impl NftCell { validate_port(*src_port)?; validate_port(*dst_port)?; } + NftCell::Drop { + 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) = 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()); + } + validate_ip_address(ip, "源IP")?; + } + + if let Some(ip) = dst_ip { + if ip.trim().is_empty() { + return Err("目标IP不能为空".to_string()); + } + validate_ip_address(ip, "目标IP")?; + } + } } Ok(()) } @@ -481,6 +732,16 @@ fn validate_port(port: u16) -> Result<(), String> { Ok(()) } +/// 验证IP地址格式 +fn validate_ip_address(ip: &str, field_name: &str) -> Result<(), String> { + // 尝试解析为 IpNetwork(支持 CIDR 表示法) + if ipnetwork::IpNetwork::from_str(ip).is_ok() { + Ok(()) + } else { + Err(format!("{}地址 '{}' 格式无效", field_name, ip)) + } +} + /// 验证legacy格式配置内容 /// 返回第一个遇到的错误,跳过注释和空行 pub fn validate_legacy_config(content: &str) -> Result<(), String> { @@ -692,4 +953,194 @@ ip_version = "all" let result = validate_legacy_config(content); assert!(result.is_err()); } + + #[test] + 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, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_drop_with_ipv6_address() { + let rule = NftCell::Drop { + 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, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + 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, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + 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_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()), + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + 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_drop_all_with_ipv4_address() { + let rule = NftCell::Drop { + 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, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_drop_all_with_ipv6_address() { + let rule = NftCell::Drop { + 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, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + 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, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_drop_ipv6_cidr_notation() { + let rule = NftCell::Drop { + 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, + comment: None, + }; + assert!(rule.validate().is_ok()); + } + + #[test] + fn test_drop_invalid_ip_address() { + let rule = NftCell::Drop { + 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, + comment: None, + }; + let result = rule.validate(); + assert!(result.is_err()); + let err_msg = result.unwrap_err(); + assert!(err_msg.contains("格式无效")); + } + + #[test] + 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, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + comment: None, + }; + let result = rule.validate(); + assert!(result.is_err()); + let err_msg = result.unwrap_err(); + assert!(err_msg.contains("格式无效")); + } + + #[test] + 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, + src_port: None, + src_port_end: None, + dst_port: None, + dst_port_end: None, + protocol: Protocol::All, + comment: None, + }; + assert!(rule.validate().is_ok()); + } } diff --git a/nat-console/src/config.rs b/nat-console/src/config.rs index 91776dc..b73c097 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 Drop 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 Drop 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 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 )) } 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