1use crate::semantic::SemanticParser;
2use crate::tree::parse_raw_tree;
3use netconv_core::ir::NetworkConfig;
4use netconv_core::report::ConversionReport;
5use netconv_core::traits::ConfigParser;
6
7pub struct IosParser;
8
9impl ConfigParser for IosParser {
10 type Error = IosParseError;
11
12 fn parse(&self, input: &str) -> Result<(NetworkConfig, ConversionReport), Self::Error> {
13 let mut report = ConversionReport::new("Cisco IOS", "");
14
15 let tree = parse_raw_tree(input);
17
18 let semantic = SemanticParser;
20 let config = semantic.analyze(&tree, &mut report);
21
22 Ok((config, report))
23 }
24
25 fn vendor_name(&self) -> &str {
26 "Cisco IOS"
27 }
28}
29
30#[derive(Debug)]
31pub enum IosParseError {
32 Empty,
33}
34
35impl std::fmt::Display for IosParseError {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(f, "IOS parse error: empty input")
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 const SAMPLE_CONFIG: &str = r#"
46hostname CORE-RTR-01
47ip domain-name corp.local
48!
49interface GigabitEthernet0/0
50 description ** WAN Uplink **
51 ip address 203.0.113.2 255.255.255.252
52 ip nat outside
53 no shutdown
54!
55interface GigabitEthernet0/1
56 description ** LAN **
57 ip address 10.0.0.1 255.255.255.0
58 ip nat inside
59 ip helper-address 10.0.0.254
60 no shutdown
61!
62interface Loopback0
63 description Router-ID
64 ip address 1.1.1.1 255.255.255.255
65 no shutdown
66!
67router ospf 1
68 router-id 1.1.1.1
69 log-adjacency-changes
70 network 10.0.0.0 0.0.0.255 area 0
71 network 1.1.1.1 0.0.0.0 area 0
72 passive-interface GigabitEthernet0/0
73 default-information originate always
74!
75ip route 0.0.0.0 0.0.0.0 203.0.113.1
76ip route 192.168.100.0 255.255.255.0 10.0.0.254 name BRANCH-1
77!
78ip access-list extended ACL-INTERNET-IN
79 10 permit tcp any host 203.0.113.2 eq 443
80 20 permit tcp any host 203.0.113.2 eq 80
81 30 deny ip any any log
82!
83ip nat inside source list ACL-NAT interface GigabitEthernet0/0 overload
84!
85ntp server 216.239.35.0 prefer
86ntp server 216.239.35.4
87!
88snmp-server community public RO
89snmp-server location Moscow DC-1
90"#;
91
92 #[test]
93 fn test_parse_hostname() {
94 let parser = IosParser;
95 let (cfg, _) = parser.parse(SAMPLE_CONFIG).unwrap();
96 assert_eq!(cfg.hostname, Some("CORE-RTR-01".to_string()));
97 }
98
99 #[test]
100 fn test_parse_interfaces() {
101 let parser = IosParser;
102 let (cfg, _) = parser.parse(SAMPLE_CONFIG).unwrap();
103 assert_eq!(cfg.interfaces.len(), 3);
104
105 let ge00 = cfg
106 .interfaces
107 .iter()
108 .find(|i| i.name.original.contains("GigabitEthernet0/0"))
109 .unwrap();
110 assert!(!ge00.shutdown);
111 assert_eq!(ge00.addresses.len(), 1);
112 assert_eq!(ge00.addresses[0].prefix.to_string(), "203.0.113.2/30");
113 assert_eq!(
114 ge00.nat_direction,
115 Some(netconv_core::ir::NatDirection::Outside)
116 );
117 }
118
119 #[test]
120 fn test_parse_ospf() {
121 let parser = IosParser;
122 let (cfg, _) = parser.parse(SAMPLE_CONFIG).unwrap();
123 assert_eq!(cfg.routing.ospf.len(), 1);
124
125 let ospf = &cfg.routing.ospf[0];
126 assert_eq!(ospf.process_id, 1);
127 assert_eq!(ospf.router_id, Some("1.1.1.1".parse().unwrap()));
128 assert!(ospf.log_adjacency);
129 assert!(ospf.default_originate.is_some());
130 assert!(ospf.default_originate.as_ref().unwrap().always);
131 assert_eq!(ospf.passive_interfaces, vec!["GigabitEthernet0/0"]);
132 }
133
134 #[test]
135 fn test_parse_static_routes() {
136 let parser = IosParser;
137 let (cfg, _) = parser.parse(SAMPLE_CONFIG).unwrap();
138 assert_eq!(cfg.routing.static_routes.len(), 2);
139
140 let default_route = cfg
141 .routing
142 .static_routes
143 .iter()
144 .find(|r| r.prefix.to_string() == "0.0.0.0/0")
145 .unwrap();
146 assert!(matches!(
147 default_route.next_hop,
148 netconv_core::ir::NextHop::Ip(_)
149 ));
150
151 let branch = cfg
152 .routing
153 .static_routes
154 .iter()
155 .find(|r| r.name == Some("BRANCH-1".to_string()))
156 .unwrap();
157 assert_eq!(branch.prefix.to_string(), "192.168.100.0/24");
158 }
159
160 #[test]
161 fn test_parse_acl() {
162 let parser = IosParser;
163 let (cfg, _) = parser.parse(SAMPLE_CONFIG).unwrap();
164 assert_eq!(cfg.acls.len(), 1);
165
166 let acl = &cfg.acls[0];
167 assert!(matches!(&acl.name, netconv_core::ir::AclName::Named(n) if n == "ACL-INTERNET-IN"));
168 assert_eq!(acl.entries.len(), 3);
169 assert!(acl.entries[2].log);
170 }
171
172 #[test]
173 fn test_parse_ntp() {
174 let parser = IosParser;
175 let (cfg, _) = parser.parse(SAMPLE_CONFIG).unwrap();
176 assert_eq!(cfg.ntp.len(), 2);
177 assert!(cfg.ntp[0].prefer);
178 }
179
180 const BGP_CONFIG: &str = r#"
182hostname BGP-RTR-01
183!
184router bgp 65001
185 bgp router-id 1.1.1.1
186 bgp log-neighbor-changes
187 neighbor IBGP-PEERS peer-group
188 neighbor IBGP-PEERS remote-as 65001
189 neighbor IBGP-PEERS update-source Loopback0
190 neighbor IBGP-PEERS next-hop-self
191 neighbor 10.0.0.2 peer-group IBGP-PEERS
192 neighbor 10.0.0.2 description RR-CLIENT-MSK
193 neighbor 10.0.0.3 remote-as 65002
194 neighbor 10.0.0.3 description EBGP-PEER
195 neighbor 10.0.0.3 route-map RM-IN in
196 neighbor 10.0.0.3 route-map RM-OUT out
197 neighbor 10.0.0.3 send-community
198 neighbor 10.0.0.3 remove-private-as
199 !
200 address-family ipv4
201 network 192.168.0.0 mask 255.255.255.0
202 network 10.0.0.0 mask 255.255.0.0
203 redistribute connected
204 aggregate-address 10.0.0.0 255.255.0.0 summary-only
205 neighbor IBGP-PEERS activate
206 neighbor 10.0.0.2 activate
207 neighbor 10.0.0.3 activate
208 neighbor 10.0.0.3 soft-reconfiguration inbound
209 neighbor 10.0.0.3 default-originate
210 exit-address-family
211"#;
212
213 #[test]
214 fn test_bgp_basic() {
215 let parser = IosParser;
216 let (cfg, _) = parser.parse(BGP_CONFIG).unwrap();
217 let bgp = cfg.routing.bgp.as_ref().unwrap();
218 assert_eq!(bgp.asn, 65001);
219 assert_eq!(bgp.router_id, Some("1.1.1.1".parse().unwrap()));
220 assert!(bgp.log_neighbor_changes);
221 }
222
223 #[test]
224 fn test_bgp_peer_group() {
225 let parser = IosParser;
226 let (cfg, _) = parser.parse(BGP_CONFIG).unwrap();
227 let bgp = cfg.routing.bgp.as_ref().unwrap();
228 assert_eq!(bgp.peer_groups.len(), 1);
229 assert_eq!(bgp.peer_groups[0].name, "IBGP-PEERS");
230 }
231
232 #[test]
233 fn test_bgp_neighbors() {
234 let parser = IosParser;
235 let (cfg, _) = parser.parse(BGP_CONFIG).unwrap();
236 let bgp = cfg.routing.bgp.as_ref().unwrap();
237
238 let n02 = bgp
239 .neighbors
240 .iter()
241 .find(|n| {
242 n.address == netconv_core::ir::BgpNeighborAddr::Ip("10.0.0.2".parse().unwrap())
243 })
244 .unwrap();
245 assert_eq!(n02.peer_group, Some("IBGP-PEERS".to_string()));
246 assert_eq!(n02.description, Some("RR-CLIENT-MSK".to_string()));
247
248 let n03 = bgp
249 .neighbors
250 .iter()
251 .find(|n| {
252 n.address == netconv_core::ir::BgpNeighborAddr::Ip("10.0.0.3".parse().unwrap())
253 })
254 .unwrap();
255 assert_eq!(n03.remote_as, 65002);
256 assert_eq!(n03.route_map_in, Some("RM-IN".to_string()));
257 assert_eq!(n03.route_map_out, Some("RM-OUT".to_string()));
258 assert!(n03.send_community);
259 assert!(n03.remove_private_as);
260 }
261
262 #[test]
263 fn test_bgp_address_family() {
264 let parser = IosParser;
265 let (cfg, _) = parser.parse(BGP_CONFIG).unwrap();
266 let bgp = cfg.routing.bgp.as_ref().unwrap();
267
268 assert_eq!(bgp.address_families.len(), 1);
269 let af = &bgp.address_families[0];
270 assert_eq!(af.afi, netconv_core::ir::BgpAfi::Ipv4);
271 assert_eq!(af.safi, netconv_core::ir::BgpSafi::Unicast);
272 assert_eq!(af.networks.len(), 2);
273 assert!(af
274 .networks
275 .iter()
276 .any(|n| n.to_string() == "192.168.0.0/24"));
277 assert!(af.networks.iter().any(|n| n.to_string() == "10.0.0.0/16"));
278 assert_eq!(af.redistribute.len(), 1);
279 assert!(matches!(
280 af.redistribute[0].source,
281 netconv_core::ir::RedistributeSource::Connected
282 ));
283 assert_eq!(af.aggregate_addresses.len(), 1);
284 assert!(af.aggregate_addresses[0].summary_only);
285 assert_eq!(af.activated_neighbors.len(), 3);
286
287 let n03_af = af
288 .neighbor_settings
289 .iter()
290 .find(|n| {
291 n.address == netconv_core::ir::BgpNeighborAddr::Ip("10.0.0.3".parse().unwrap())
292 })
293 .unwrap();
294 assert!(n03_af.soft_reconfiguration);
295 assert!(n03_af.default_originate);
296 }
297
298 #[test]
299 fn test_bgp_no_unknown_address_family() {
300 let parser = IosParser;
301 let (_, report) = parser.parse(BGP_CONFIG).unwrap();
302 let af_unknowns: Vec<_> = report
303 .items
304 .iter()
305 .filter(|i| {
306 i.severity == netconv_core::report::Severity::Info
307 && i.source_snippet.contains("address-family")
308 })
309 .collect();
310 assert!(
311 af_unknowns.is_empty(),
312 "address-family в unknown: {:?}",
313 af_unknowns
314 );
315 }
316
317 const NAT_POOL_BEFORE_REF: &str = r#"
322hostname NAT-RTR
323!
324ip nat pool OFFICE-POOL 192.168.1.10 192.168.1.20 prefix-length 24
325ip nat inside source list NAT-ACL pool OFFICE-POOL overload
326!
327ip access-list standard NAT-ACL
328 10 permit 10.0.0.0 0.0.0.255
329"#;
330
331 const NAT_POOL_AFTER_REF: &str = r#"
332hostname NAT-RTR
333!
334ip nat inside source list NAT-ACL pool OFFICE-POOL overload
335ip nat pool OFFICE-POOL 192.168.1.10 192.168.1.20 prefix-length 24
336!
337ip access-list standard NAT-ACL
338 10 permit 10.0.0.0 0.0.0.255
339"#;
340
341 const NAT_POOL_UNDEFINED: &str = r#"
342hostname NAT-RTR
343!
344ip nat inside source list NAT-ACL pool MISSING-POOL overload
345!
346ip access-list standard NAT-ACL
347 10 permit 10.0.0.0 0.0.0.255
348"#;
349
350 #[test]
351 fn test_nat_pool_resolved_when_declared_before_reference() {
352 let parser = IosParser;
353 let (cfg, _) = parser.parse(NAT_POOL_BEFORE_REF).unwrap();
354 assert_eq!(cfg.nat.len(), 1);
355 let pool = cfg.nat[0].pool.as_ref().expect("pool should be resolved");
356 assert_eq!(pool.name, "OFFICE-POOL");
357 assert_eq!(
358 pool.start,
359 "192.168.1.10".parse::<std::net::IpAddr>().unwrap()
360 );
361 assert_eq!(
362 pool.end,
363 "192.168.1.20".parse::<std::net::IpAddr>().unwrap()
364 );
365 assert!(pool.prefix.is_some());
366 }
367
368 #[test]
369 fn test_nat_pool_resolved_when_declared_after_reference() {
370 let parser = IosParser;
373 let (cfg, _) = parser.parse(NAT_POOL_AFTER_REF).unwrap();
374 assert_eq!(cfg.nat.len(), 1);
375 let pool = cfg.nat[0].pool.as_ref().expect("pool should be resolved");
376 assert_eq!(
377 pool.start,
378 "192.168.1.10".parse::<std::net::IpAddr>().unwrap()
379 );
380 assert_eq!(
381 pool.end,
382 "192.168.1.20".parse::<std::net::IpAddr>().unwrap()
383 );
384 }
385
386 #[test]
387 fn test_nat_pool_undefined_marked_manual_not_silently_zeroed() {
388 let parser = IosParser;
389 let (cfg, report) = parser.parse(NAT_POOL_UNDEFINED).unwrap();
390 assert_eq!(cfg.nat.len(), 1);
391 let manual_items: Vec<_> = report
394 .items
395 .iter()
396 .filter(|i| i.category == "nat.pool_undefined")
397 .collect();
398 assert_eq!(
399 manual_items.len(),
400 1,
401 "должен быть один manual item про неопределённый пул"
402 );
403 assert_eq!(
404 manual_items[0].severity,
405 netconv_core::report::Severity::Error
406 );
407 }
408
409 #[test]
410 fn test_nat_pool_declaration_not_marked_unknown() {
411 let parser = IosParser;
414 let (cfg, report) = parser.parse(NAT_POOL_BEFORE_REF).unwrap();
415 let pool_unknowns: Vec<_> = cfg
416 .unknown_blocks
417 .iter()
418 .filter(|b| b.raw.contains("ip nat pool"))
419 .collect();
420 assert!(
421 pool_unknowns.is_empty(),
422 "ip nat pool не должен быть в unknown_blocks: {:?}",
423 pool_unknowns
424 );
425
426 let pool_unknown_reports: Vec<_> = report
427 .items
428 .iter()
429 .filter(|i| {
430 i.source_snippet.contains("ip nat pool")
431 && i.severity == netconv_core::report::Severity::Info
432 })
433 .collect();
434 assert!(
435 pool_unknown_reports.is_empty(),
436 "ip nat pool не должен попадать в unknown report items"
437 );
438 }
439
440 #[test]
446 fn test_acl_port_after_host_is_parsed() {
447 let config = r#"
448hostname RTR
449!
450ip access-list extended ACL-TEST
451 10 permit tcp any host 203.0.113.2 eq 443
452 20 permit tcp host 10.0.0.5 eq 22 any
453"#;
454 let parser = IosParser;
455 let (cfg, _) = parser.parse(config).unwrap();
456 assert_eq!(cfg.acls.len(), 1);
457 let entries = &cfg.acls[0].entries;
458 assert_eq!(entries.len(), 2);
459
460 assert!(
462 matches!(
463 entries[0].dst_port,
464 Some(netconv_core::ir::AclPort::Eq(443))
465 ),
466 "expected dst_port = Eq(443), got {:?}",
467 entries[0].dst_port
468 );
469
470 assert!(
472 matches!(entries[1].src_port, Some(netconv_core::ir::AclPort::Eq(22))),
473 "expected src_port = Eq(22), got {:?}",
474 entries[1].src_port
475 );
476 }
477
478 #[test]
479 fn test_acl_entries_appear_in_report() {
480 let parser = IosParser;
481 let (cfg, _) = parser.parse(NAT_POOL_BEFORE_REF).unwrap();
482 assert_eq!(cfg.acls.len(), 1);
483 }
484
485 #[test]
491 fn test_no_ip_address_does_not_clear_other_ip_settings() {
492 let config = r#"
493hostname RTR
494!
495interface GigabitEthernet0/0
496 ip address 10.0.0.1 255.255.255.0
497 no ip redirects
498 no ip address
499 no shutdown
500"#;
501 let parser = IosParser;
502 let (cfg, _) = parser.parse(config).unwrap();
503 let iface = &cfg.interfaces[0];
504 assert_eq!(iface.addresses.len(), 1);
508 assert!(!iface.shutdown);
509 }
510}