1use 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#[derive(Debug, Default, Deserialize, Clone, Copy, PartialEq)]
25#[serde(rename_all = "lowercase")]
26pub enum IntranetMergeMode {
27 #[default]
29 Add,
30 Replace,
32}
33
34#[derive(Debug, Deserialize, Clone)]
38pub struct IntranetNetsConf {
39 #[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
52static INTRANET_NETS_CONF: RwLock<Option<IntranetNetsConf>> = RwLock::new(None);
54
55pub fn set_intranet_nets_conf(conf: Option<IntranetNetsConf>) {
57 if let Ok(mut guard) = INTRANET_NETS_CONF.write() {
58 *guard = conf;
59 }
60}
61
62const DEFAULT_INTRANET_NETS: &[&str] = &[
67 "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "::1/128", "fc00::/7", ];
74
75#[derive(Debug)]
77pub struct IntranetNetsSet {
78 v4_nets: Vec<Ipv4Net>,
80 v6_nets: Vec<Ipv6Net>,
82}
83
84impl IntranetNetsSet {
85 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 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 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 pub fn net_count(&self) -> usize {
124 self.v4_nets.len() + self.v6_nets.len()
125 }
126}
127
128fn 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
144pub 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
158pub 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
169pub 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
192pub 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
222pub 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]))); assert!(!set.contains(&v4([11, 0, 0, 1])));
266 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 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 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 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 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]))); }
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 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 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}