Skip to main content

wp_knowledge/
intranet_nets.rs

1//! 内网网段知识管理
2//!
3//! 内网网段(哪些 IP 段属于内网)属于知识信息,由本模块统一管理。
4//! 配置直接放在 `knowdb.toml` 的 `[intranet_nets]` 节(与知识库其他配置同源),
5//! 由 knowdb 加载流程解析后通过 [`set_intranet_nets_conf`] 注入;
6//! 底层以内存 `ipnet` 集合实现(量小、高频判断),规则引擎(wp-oml)通过
7//! [`is_intranet`] 消费。
8//!
9//! **时序约定**:`INTRANET_NETS_SET` 在首次调用 [`is_intranet`] 时按已注入的配置
10//! 初始化。因此 knowdb 加载(注入配置)必须先于任何内网判断调用——引擎流程已保证
11//! 这一点(knowdb 在规则执行前加载);若工具在 knowdb 加载前调用 [`is_intranet`],
12//! 将得到内置默认网段(无外部配置)。
13
14use once_cell::sync::Lazy;
15use serde::Deserialize;
16use std::net::IpAddr;
17use std::path::Path;
18use std::str::FromStr;
19use std::sync::RwLock;
20
21use ipnet::{IpNet, Ipv4Net, Ipv6Net};
22
23/// 配置合并模式
24#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq)]
25#[serde(rename_all = "lowercase")]
26pub enum IntranetMergeMode {
27    /// 添加模式:外部配置网段添加到内置默认网段(默认)
28    #[default]
29    Add,
30    /// 替换模式:外部配置网段完全替换内置默认网段
31    Replace,
32}
33
34/// 内网网段配置(`knowdb.toml` 的 `[intranet_nets]` 节)
35///
36/// 版本继承 `knowdb.toml` 文件级 `version = 2`,本节不再单独声明。
37#[derive(Debug, Deserialize, Clone)]
38pub struct IntranetNetsConf {
39    /// 外部配置是否生效(默认 true,即提供配置即生效;设为 false 忽略外部网段)
40    #[serde(default = "default_intranet_nets_enabled", alias = "enable")]
41    pub enabled: bool,
42    #[serde(default)]
43    pub mode: IntranetMergeMode,
44    #[serde(default)]
45    pub nets: Vec<String>,
46}
47
48fn default_intranet_nets_enabled() -> bool {
49    true
50}
51
52/// 外部配置注入(由 knowdb.toml 加载流程调用)
53static INTRANET_NETS_CONF: RwLock<Option<IntranetNetsConf>> = RwLock::new(None);
54
55/// 设置内网网段配置(knowdb 加载流程解析 `[intranet_nets]` 节后调用)
56pub fn set_intranet_nets_conf(conf: Option<IntranetNetsConf>) {
57    if let Ok(mut guard) = INTRANET_NETS_CONF.write() {
58        *guard = conf;
59    }
60}
61
62/// 内网 Nets 默认网段全集(RFC1918 + IPv4/IPv6 loopback + IPv6 ULA)
63///
64/// 注意:CGNAT(100.64.0.0/10)、link-local(169.254.0.0/16、fe80::/10)等
65/// 既非内网也非互联网的特殊地址不包含在默认集合内,由外部配置按需扩展。
66const DEFAULT_INTRANET_NETS: &[&str] = &[
67    "10.0.0.0/8",     // RFC1918
68    "172.16.0.0/12",  // RFC1918
69    "192.168.0.0/16", // RFC1918
70    "127.0.0.0/8",    // IPv4 loopback
71    "::1/128",        // IPv6 loopback
72    "fc00::/7",       // IPv6 ULA
73];
74
75/// 内网 Nets 运行时集合(内存判断,非表机制)
76#[derive(Debug)]
77pub struct IntranetNetsSet {
78    /// IPv4 网段桶
79    v4_nets: Vec<Ipv4Net>,
80    /// IPv6 网段桶
81    v6_nets: Vec<Ipv6Net>,
82}
83
84impl IntranetNetsSet {
85    /// 创建内置默认网段集合(RFC1918 + IPv4/IPv6 loopback + IPv6 ULA)
86    pub fn builtin() -> Self {
87        let (v4_nets, v6_nets) = split_nets(
88            DEFAULT_INTRANET_NETS
89                .iter()
90                .filter_map(|s| IpNet::from_str(s).ok()),
91        );
92        Self { v4_nets, v6_nets }
93    }
94
95    /// 合并外部配置(非法网段静默忽略)
96    pub fn merge(&mut self, conf: IntranetNetsConf) {
97        let (v4, v6) = split_nets(
98            conf.nets
99                .iter()
100                .filter_map(|c| IpNet::from_str(c.trim()).ok()),
101        );
102        match conf.mode {
103            IntranetMergeMode::Add => {
104                self.v4_nets.extend(v4);
105                self.v6_nets.extend(v6);
106            }
107            IntranetMergeMode::Replace => {
108                self.v4_nets = v4;
109                self.v6_nets = v6;
110            }
111        }
112    }
113
114    /// 判断 IP 是否命中内网网段(按地址族分桶,仅扫描对应家族,避免全量扫描)
115    pub fn contains(&self, ip: &IpAddr) -> bool {
116        match ip {
117            IpAddr::V4(v4) => self.v4_nets.iter().any(|n| n.contains(v4)),
118            IpAddr::V6(v6) => self.v6_nets.iter().any(|n| n.contains(v6)),
119        }
120    }
121
122    /// 当前网段数量(供 init 诊断输出)
123    pub fn net_count(&self) -> usize {
124        self.v4_nets.len() + self.v6_nets.len()
125    }
126}
127
128/// 按地址族将网段分桶(IPv4 / IPv6)
129fn split_nets<I>(nets: I) -> (Vec<Ipv4Net>, Vec<Ipv6Net>)
130where
131    I: IntoIterator<Item = IpNet>,
132{
133    let mut v4 = Vec::new();
134    let mut v6 = Vec::new();
135    for net in nets {
136        match net {
137            IpNet::V4(x) => v4.push(x),
138            IpNet::V6(x) => v6.push(x),
139        }
140    }
141    (v4, v6)
142}
143
144/// 全局内网 Nets 集合(Lazy 延迟加载:内置默认 + knowdb 注入的外部配置合并)
145pub static INTRANET_NETS_SET: Lazy<IntranetNetsSet> = Lazy::new(|| {
146    let mut set = IntranetNetsSet::builtin();
147
148    if let Ok(guard) = INTRANET_NETS_CONF.read()
149        && let Some(conf) = guard.as_ref()
150        && conf.enabled
151    {
152        set.merge(conf.clone());
153    }
154
155    set
156});
157
158/// 判断 IP 是否为内网地址(命中内网网段集合)
159///
160/// IPv4-mapped IPv6(`::ffff:a.b.c.d`)按 IPv4 判定,避免映射地址被误判为外网。
161pub fn is_intranet(ip: &IpAddr) -> bool {
162    let ip = match ip {
163        IpAddr::V6(v6) => v6.to_ipv4_mapped().map(IpAddr::V4).unwrap_or(*ip),
164        _ => *ip,
165    };
166    INTRANET_NETS_SET.contains(&ip)
167}
168
169/// 启动预加载(触发 INTRANET_NETS_SET 初始化),返回诊断消息
170///
171/// 供引擎启动诊断/运维调用;正常规则执行无需显式调用(Lazy 在首次判断时自然初始化)。
172pub fn init_intranet_nets() -> Result<String, String> {
173    let set = &*INTRANET_NETS_SET;
174    let external = INTRANET_NETS_CONF
175        .read()
176        .map(|g| g.is_some())
177        .unwrap_or(false);
178
179    if external {
180        Ok(format!(
181            "内网 Nets 已加载 | 外部配置生效 | 网段数: {}",
182            set.net_count()
183        ))
184    } else {
185        Ok(format!(
186            "内网 Nets 已加载 | 使用内置网段 | 网段数: {}",
187            set.net_count()
188        ))
189    }
190}
191
192/// 从 `knowdb.toml` 检查 `[intranet_nets]` 节(供 wproj check)
193///
194/// 直接从文件解析,不依赖引擎加载;返回 `Ok(None)` 表示未配置(使用内置网段),
195/// `Ok(Some(msg))` 表示外部配置生效。
196pub fn check_intranet_nets_config(config_path: &Path) -> Result<Option<String>, String> {
197    let content =
198        std::fs::read_to_string(config_path).map_err(|e| format!("读取 knowdb 配置失败: {}", e))?;
199    let value: toml::Value =
200        toml::from_str(&content).map_err(|e| format!("解析 knowdb 配置失败: {}", e))?;
201    let Some(section) = value.get("intranet_nets") else {
202        return Ok(None);
203    };
204    let conf: IntranetNetsConf = section
205        .clone()
206        .try_into()
207        .map_err(|e| format!("解析 [intranet_nets] 节失败: {}", e))?;
208    if !conf.enabled {
209        return Ok(None);
210    }
211    let mode = match conf.mode {
212        IntranetMergeMode::Add => "add",
213        IntranetMergeMode::Replace => "replace",
214    };
215    Ok(Some(format!(
216        "内网网段配置生效 | {} 个网段 | mode={}",
217        conf.nets.len(),
218        mode
219    )))
220}
221
222/// 生成内网网段配置的 `[intranet_nets]` 节(供项目初始化拼入 `knowdb.toml`)
223pub fn generate_default_intranet_nets_config() -> String {
224    r#"
225[intranet_nets]
226# 内网网段知识配置:扩展或替换系统内置网段(RFC1918 + IPv4/IPv6 loopback + IPv6 ULA)
227#   enabled:外部配置开关(默认 true)
228#   mode:"add" 添加到内置网段(推荐)/ "replace" 完全替换
229#   nets:内网网段列表(CIDR 写法,支持 IPv4 / IPv6)
230#         示例:nets = ["172.32.0.0/16"](企业专有/资产网段)
231enabled = true
232mode = "add"
233nets = []
234"#
235    .to_string()
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use std::net::{Ipv4Addr, Ipv6Addr};
242
243    fn v4(octets: [u8; 4]) -> IpAddr {
244        IpAddr::V4(Ipv4Addr::from(octets))
245    }
246
247    fn v6(segments: [u16; 8]) -> IpAddr {
248        IpAddr::V6(Ipv6Addr::from(segments))
249    }
250
251    #[test]
252    fn builtin_set_contains_private_ipv4() {
253        let set = IntranetNetsSet::builtin();
254        assert!(set.contains(&v4([10, 0, 0, 1])));
255        assert!(set.contains(&v4([172, 16, 0, 1])));
256        assert!(set.contains(&v4([192, 168, 1, 1])));
257        assert!(set.contains(&v4([127, 0, 0, 1])));
258    }
259
260    #[test]
261    fn builtin_set_excludes_public_and_special_ipv4() {
262        let set = IntranetNetsSet::builtin();
263        assert!(!set.contains(&v4([8, 8, 8, 8])));
264        assert!(!set.contains(&v4([172, 32, 0, 1]))); // 172.32 不在 RFC1918 12 位内
265        assert!(!set.contains(&v4([11, 0, 0, 1])));
266        // CGNAT / link-local 属特殊地址,默认不判为内网
267        assert!(!set.contains(&v4([100, 64, 1, 1])));
268        assert!(!set.contains(&v4([169, 254, 1, 1])));
269    }
270
271    #[test]
272    fn builtin_set_contains_private_ipv6() {
273        let set = IntranetNetsSet::builtin();
274        // fc00::/7 ULA
275        assert!(set.contains(&v6([0xfc00, 0, 0, 0, 0, 0, 0, 1])));
276        assert!(set.contains(&v6([0xfd00, 0, 0, 0, 0, 0, 0, 1])));
277        // ::1 loopback
278        assert!(set.contains(&v6([0, 0, 0, 0, 0, 0, 0, 1])));
279    }
280
281    #[test]
282    fn builtin_set_excludes_public_and_special_ipv6() {
283        let set = IntranetNetsSet::builtin();
284        assert!(!set.contains(&v6([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888])));
285        assert!(!set.contains(&v6([0x2001, 0xdb8, 0, 0, 0, 0, 0, 1])));
286        // fe80::/10 link-local 默认不判为内网
287        assert!(!set.contains(&v6([0xfe80, 0, 0, 0, 0, 0, 0, 1])));
288    }
289
290    #[test]
291    fn merge_add_appends_custom_nets() {
292        let mut set = IntranetNetsSet::builtin();
293        set.merge(IntranetNetsConf {
294            enabled: true,
295            mode: IntranetMergeMode::Add,
296            nets: vec!["172.32.0.0/16".to_string(), "192.0.2.0/24".to_string()],
297        });
298        assert!(set.contains(&v4([172, 32, 0, 1])));
299        assert!(set.contains(&v4([192, 0, 2, 1])));
300        // 默认网段仍保留
301        assert!(set.contains(&v4([10, 0, 0, 1])));
302    }
303
304    #[test]
305    fn merge_replace_overrides_builtin() {
306        let mut set = IntranetNetsSet::builtin();
307        set.merge(IntranetNetsConf {
308            enabled: true,
309            mode: IntranetMergeMode::Replace,
310            nets: vec!["172.32.0.0/16".to_string()],
311        });
312        assert!(set.contains(&v4([172, 32, 0, 1])));
313        assert!(!set.contains(&v4([10, 0, 0, 1]))); // 默认网段被替换
314    }
315
316    #[test]
317    fn merge_ignores_invalid_nets() {
318        let mut set = IntranetNetsSet::builtin();
319        let before = set.net_count();
320        set.merge(IntranetNetsConf {
321            enabled: true,
322            mode: IntranetMergeMode::Add,
323            nets: vec!["not-a-cidr".to_string(), "10.0.0.0/999".to_string()],
324        });
325        assert_eq!(set.net_count(), before, "非法 Nets 应被忽略");
326    }
327
328    #[test]
329    fn toml_enabled_defaults_to_true() {
330        // 未写 enabled 的配置 → 默认 true(提供配置即生效)
331        let toml_str = r#"
332            nets = ["172.32.0.0/16"]
333        "#;
334        let conf: IntranetNetsConf = toml::from_str(toml_str).unwrap();
335        assert!(conf.enabled);
336        assert_eq!(conf.nets, vec!["172.32.0.0/16".to_string()]);
337    }
338
339    #[test]
340    fn toml_explicit_enabled_false_ignored() {
341        let toml_str = r#"
342            enabled = false
343            nets = ["172.32.0.0/16"]
344        "#;
345        let conf: IntranetNetsConf = toml::from_str(toml_str).unwrap();
346        assert!(!conf.enabled);
347    }
348
349    #[test]
350    fn is_intranet_ipv4_mapped_ipv6() {
351        // ::ffff:192.168.0.1 → 按 IPv4 判定为内网
352        let mapped: IpAddr = "::ffff:192.168.0.1".parse().unwrap();
353        assert!(is_intranet(&mapped));
354        let mapped_public: IpAddr = "::ffff:8.8.8.8".parse().unwrap();
355        assert!(!is_intranet(&mapped_public));
356    }
357
358    #[test]
359    fn generate_default_config_contains_nets_field() {
360        let cfg = generate_default_intranet_nets_config();
361        assert!(cfg.contains("[intranet_nets]"));
362        assert!(cfg.contains("nets ="));
363        assert!(cfg.contains("172.32.0.0/16"));
364    }
365
366    #[test]
367    fn check_intranet_nets_config_reads_section() {
368        let path = std::env::temp_dir().join(format!(
369            "wpk_intranet_check_section_{}.toml",
370            std::process::id()
371        ));
372        std::fs::write(
373            &path,
374            "version = 2\n[intranet_nets]\nenabled = true\nmode = \"add\"\nnets = [\"172.32.0.0/16\"]\n",
375        )
376        .unwrap();
377        let msg = check_intranet_nets_config(&path)
378            .unwrap()
379            .expect("should report config effective");
380        assert!(msg.contains("1 个网段"), "msg: {}", msg);
381        std::fs::remove_file(&path).ok();
382    }
383
384    #[test]
385    fn check_intranet_nets_config_missing_section() {
386        let path = std::env::temp_dir().join(format!(
387            "wpk_intranet_check_missing_{}.toml",
388            std::process::id()
389        ));
390        std::fs::write(&path, "version = 2\n").unwrap();
391        let r = check_intranet_nets_config(&path).unwrap();
392        assert!(r.is_none());
393        std::fs::remove_file(&path).ok();
394    }
395}