From bf79a2ee4923b2c69dc386db0a65fc5312920ffb Mon Sep 17 00:00:00 2001 From: Dan Halperin Date: Tue, 5 May 2026 18:55:06 -0700 Subject: [PATCH] junos_commit_check: add IPv6 prefix normalization tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add IPv6 variants of all commit check tests. Junos applies the same prefix normalization enforcement (rejecting host bits) to IPv6 prefixes in most inet6 contexts: static/aggregate/generate routes, OSPFv3 area-range, and condition if-route-exists with inet6.0 table. Notable finding: unlike IPv4 firewall, Junos does NOT reject host bits in `firewall family inet6 filter` destination-address or next-ip6. These are placed in the "accepts" category. Also adds IPv6 "accepts" cases: prefix-list, route-filter, interface address, BGP allow, and the firewall inet6 contexts above. The IPv6 "rejects" cases are sickbayed as xfail pending batfish/batfish#9934 (Batfish doesn't yet check IPv6 normalization). Also fixes the commit check framework to use `send_command` with `expect_string` instead of `send_command_timing`, and to distinguish indeterminate results (timeout, empty output, exceptions) from definitive rejections — preventing silent false passes. All tests validated on vJunos-router 25.4R1.12 via containerlab on EC2. ---- Prompt: ``` In a session earlier today, we added support for doing commit checks on Juniper and built a lab for IPv4 prefixes in various contexts. Let's also add ipv6 variants of these tests to both the commit checks and the junos_commit_checks lab. ``` --- infra/examples/junos-commit-check/README.md | 38 +++++-- infra/examples/junos-commit-check/checks.yaml | 107 ++++++++++++++++++ .../show_configuration_|_display_set.txt | 5 + .../show_configuration_|_display_set.txt | 3 + .../show_configuration_|_display_set.txt | 2 + .../show_configuration_|_display_set.txt | 2 + .../show_configuration_|_display_set.txt | 2 + .../show_configuration_|_display_set.txt | 2 + .../show_configuration_|_display_set.txt | 2 + .../show_configuration_|_display_set.txt | 3 + .../show_configuration_|_display_set.txt | 2 + .../show_configuration_|_display_set.txt | 3 + .../show_configuration_|_display_set.txt | 2 + .../junos_commit_check/show/host_nos.txt | 13 ++- .../validation/parse_warnings.yaml | 22 +++- .../validation/sickbay.yaml | 26 +++++ src/lab_builder/validate.py | 48 ++++++-- 17 files changed, 262 insertions(+), 20 deletions(-) create mode 100644 snapshots/junos_commit_check/configs/accepts-bgp-allow-v6/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/accepts-firewall6-address/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/accepts-firewall6-next-ip6/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/accepts-interface-v6/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/accepts-prefix-list-v6/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/accepts-route-filter-v6/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/rejects-aggregate-v6/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/rejects-condition-v6/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/rejects-generate-v6/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/rejects-ospf3-area-range/show_configuration_|_display_set.txt create mode 100644 snapshots/junos_commit_check/configs/rejects-static-v6/show_configuration_|_display_set.txt diff --git a/infra/examples/junos-commit-check/README.md b/infra/examples/junos-commit-check/README.md index 08273c4..c770e72 100644 --- a/infra/examples/junos-commit-check/README.md +++ b/infra/examples/junos-commit-check/README.md @@ -17,9 +17,10 @@ normalization and which do not. ## Methodology -Single vJunos-router node. For each grammar rule that accepts `ip_prefix` -or `ip_prefix_default_32`, we attempt to commit `192.168.1.111/24` (host -bits set) via `commit check`. The device either rejects it or accepts it. +Single vJunos-router node. For each grammar rule that accepts a prefix, +we attempt to commit a prefix with host bits set via `commit check`. The +device either rejects it or accepts it. IPv4 uses `192.168.1.111/24`; +IPv6 uses `2001:db8::1/32`. The `checks.yaml` uses two check types: @@ -34,12 +35,12 @@ Each check loads config lines, runs `commit check`, and rolls back. # On EC2 with containerlab + vJunos image: sudo containerlab deploy -t topology.clab.yml # Wait for health (~5 min) -python3 run_commit_checks.py checks.yaml 172.20.20.2 admin "admin@123" +python -m lab_builder validate topology.clab.yml --checks checks.yaml ``` ## Results (vJunos 25.4R1.12) -**Rejects host bits:** +### IPv4: rejects host bits - `routing-options static route` - `routing-options aggregate route` @@ -49,7 +50,7 @@ python3 run_commit_checks.py checks.yaml 172.20.20.2 admin "admin@123" - `firewall filter X term T then next-ip` - `policy-options condition X if-route-exists` -**Accepts host bits:** +### IPv4: accepts host bits - `policy-options prefix-list` - `policy-options policy-statement X from route-filter` @@ -59,7 +60,30 @@ python3 run_commit_checks.py checks.yaml 172.20.20.2 admin "admin@123" - `interfaces X unit Y family inet address` - `interfaces X unit Y family inet address A vrrp-group N track route` -**Not tested (requires vSRX):** +### IPv6: rejects host bits + +- `routing-options rib inet6.0 static route` +- `routing-options rib inet6.0 aggregate route` +- `routing-options rib inet6.0 generate route` +- `protocols ospf3 area X area-range` +- `policy-options condition X if-route-exists` (table inet6.0) + +### IPv6: accepts host bits + +- `firewall family inet6 filter X term T from destination-address` +- `firewall family inet6 filter X term T then next-ip6` +- `policy-options prefix-list` +- `policy-options policy-statement X from route-filter` +- `interfaces X unit Y family inet6 address` +- `protocols bgp group X allow` + +### Notable IPv4/IPv6 asymmetry + +Junos rejects host bits in IPv4 `firewall filter` (destination-address +and next-ip) but accepts them in `firewall family inet6 filter` +(destination-address and next-ip6). + +### Not tested (requires vSRX) - `security nat` (pool address, match address, static-nat prefix) - `security address-book` diff --git a/infra/examples/junos-commit-check/checks.yaml b/infra/examples/junos-commit-check/checks.yaml index ad035c4..03b0fbf 100644 --- a/infra/examples/junos-commit-check/checks.yaml +++ b/infra/examples/junos-commit-check/checks.yaml @@ -95,6 +95,85 @@ checks: - "set policy-options condition TEST if-route-exists table inet.0" description: "condition if-route-exists with valid prefix" + # --- Contexts where Junos REJECTS host bits (IPv6) --- + + - type: commit_check_rejects + node: dut + config_lines: + - "set routing-options rib inet6.0 static route 2001:db8::1/32 reject" + description: "IPv6 static route with host bits set" + + - type: commit_check_accepts + node: dut + config_lines: + - "set routing-options rib inet6.0 static route 2001:db8::/32 reject" + description: "IPv6 static route with valid prefix" + + - type: commit_check_rejects + node: dut + config_lines: + - "set routing-options rib inet6.0 aggregate route 2001:db8::1/32" + description: "IPv6 aggregate route with host bits set" + + - type: commit_check_accepts + node: dut + config_lines: + - "set routing-options rib inet6.0 aggregate route 2001:db8::/32" + description: "IPv6 aggregate route with valid prefix" + + - type: commit_check_rejects + node: dut + config_lines: + - "set routing-options rib inet6.0 generate route 2001:db8::1/32" + description: "IPv6 generate route with host bits set" + + - type: commit_check_accepts + node: dut + config_lines: + - "set routing-options rib inet6.0 generate route 2001:db8::/32" + description: "IPv6 generate route with valid prefix" + + - type: commit_check_rejects + node: dut + config_lines: + - "set protocols ospf3 area 0.0.0.0 interface lo0.0" + - "set protocols ospf3 area 0.0.0.0 area-range 2001:db8::1/32" + description: "OSPFv3 area-range with host bits set" + + - type: commit_check_accepts + node: dut + config_lines: + - "set protocols ospf3 area 0.0.0.0 interface lo0.0" + - "set protocols ospf3 area 0.0.0.0 area-range 2001:db8::/32" + description: "OSPFv3 area-range with valid prefix" + + - type: commit_check_accepts + node: dut + config_lines: + - "set firewall family inet6 filter TEST term T from destination-address 2001:db8::1/32" + - "set firewall family inet6 filter TEST term T then accept" + description: "firewall inet6 destination-address with host bits (accepted, unlike IPv4)" + + - type: commit_check_accepts + node: dut + config_lines: + - "set firewall family inet6 filter TEST term T then next-ip6 2001:db8::1/32" + description: "firewall next-ip6 with host bits (accepted, unlike IPv4)" + + - type: commit_check_rejects + node: dut + config_lines: + - "set policy-options condition TEST if-route-exists 2001:db8::1/32" + - "set policy-options condition TEST if-route-exists table inet6.0" + description: "condition if-route-exists IPv6 with host bits set" + + - type: commit_check_accepts + node: dut + config_lines: + - "set policy-options condition TEST if-route-exists 2001:db8::/32" + - "set policy-options condition TEST if-route-exists table inet6.0" + description: "condition if-route-exists IPv6 with valid prefix" + # --- Contexts where Junos ACCEPTS host bits --- - type: commit_check_accepts @@ -150,6 +229,34 @@ checks: - "set interfaces ge-0/0/0 unit 0 family inet address 10.0.0.1/30 vrrp-group 1 track route 192.168.1.111/24 routing-instance default priority-cost 100" description: "VRRP track route with host bits (accepted)" + # --- Contexts where Junos ACCEPTS host bits (IPv6) --- + + - type: commit_check_accepts + node: dut + config_lines: + - "set policy-options prefix-list TEST 2001:db8::1/32" + description: "prefix-list with IPv6 host bits (accepted)" + + - type: commit_check_accepts + node: dut + config_lines: + - "set policy-options policy-statement TEST term T from route-filter 2001:db8::1/32 exact" + description: "route-filter with IPv6 host bits (accepted)" + + - type: commit_check_accepts + node: dut + config_lines: + - "set interfaces ge-0/0/0 unit 0 family inet6 address 2001:db8::1/32" + description: "interface inet6 address with host bits (accepted)" + + - type: commit_check_accepts + node: dut + config_lines: + - "set protocols bgp group TEST type external" + - "set protocols bgp group TEST peer-as 65001" + - "set protocols bgp group TEST allow 2001:db8::1/32" + description: "BGP allow with IPv6 host bits (accepted)" + # TODO: Security hierarchy checks (require vSRX image) # - security nat source pool

address # - security nat source pool

address to diff --git a/snapshots/junos_commit_check/configs/accepts-bgp-allow-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-bgp-allow-v6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..22f96a9 --- /dev/null +++ b/snapshots/junos_commit_check/configs/accepts-bgp-allow-v6/show_configuration_|_display_set.txt @@ -0,0 +1,5 @@ +set system host-name accepts-bgp-allow-v6 +set routing-options autonomous-system 65000 +set protocols bgp group TEST type external +set protocols bgp group TEST peer-as 65001 +set protocols bgp group TEST allow 2001:db8::1/32 diff --git a/snapshots/junos_commit_check/configs/accepts-firewall6-address/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-firewall6-address/show_configuration_|_display_set.txt new file mode 100644 index 0000000..e1b0710 --- /dev/null +++ b/snapshots/junos_commit_check/configs/accepts-firewall6-address/show_configuration_|_display_set.txt @@ -0,0 +1,3 @@ +set system host-name accepts-firewall6-address +set firewall family inet6 filter TEST term T from destination-address 2001:db8::1/32 +set firewall family inet6 filter TEST term T then accept diff --git a/snapshots/junos_commit_check/configs/accepts-firewall6-next-ip6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-firewall6-next-ip6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..79bd1c2 --- /dev/null +++ b/snapshots/junos_commit_check/configs/accepts-firewall6-next-ip6/show_configuration_|_display_set.txt @@ -0,0 +1,2 @@ +set system host-name accepts-firewall6-next-ip6 +set firewall family inet6 filter TEST term T then next-ip6 2001:db8::1/32 diff --git a/snapshots/junos_commit_check/configs/accepts-interface-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-interface-v6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..15a7afd --- /dev/null +++ b/snapshots/junos_commit_check/configs/accepts-interface-v6/show_configuration_|_display_set.txt @@ -0,0 +1,2 @@ +set system host-name accepts-interface-v6 +set interfaces ge-0/0/0 unit 0 family inet6 address 2001:db8::1/32 diff --git a/snapshots/junos_commit_check/configs/accepts-prefix-list-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-prefix-list-v6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..625a9b5 --- /dev/null +++ b/snapshots/junos_commit_check/configs/accepts-prefix-list-v6/show_configuration_|_display_set.txt @@ -0,0 +1,2 @@ +set system host-name accepts-prefix-list-v6 +set policy-options prefix-list TEST 2001:db8::1/32 diff --git a/snapshots/junos_commit_check/configs/accepts-route-filter-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-route-filter-v6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..af127c8 --- /dev/null +++ b/snapshots/junos_commit_check/configs/accepts-route-filter-v6/show_configuration_|_display_set.txt @@ -0,0 +1,2 @@ +set system host-name accepts-route-filter-v6 +set policy-options policy-statement TEST term T from route-filter 2001:db8::1/32 exact diff --git a/snapshots/junos_commit_check/configs/rejects-aggregate-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-aggregate-v6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..3dc2445 --- /dev/null +++ b/snapshots/junos_commit_check/configs/rejects-aggregate-v6/show_configuration_|_display_set.txt @@ -0,0 +1,2 @@ +set system host-name rejects-aggregate-v6 +set routing-options rib inet6.0 aggregate route 2001:db8::1/32 diff --git a/snapshots/junos_commit_check/configs/rejects-condition-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-condition-v6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..bfdc80d --- /dev/null +++ b/snapshots/junos_commit_check/configs/rejects-condition-v6/show_configuration_|_display_set.txt @@ -0,0 +1,3 @@ +set system host-name rejects-condition-v6 +set policy-options condition TEST if-route-exists 2001:db8::1/32 +set policy-options condition TEST if-route-exists table inet6.0 diff --git a/snapshots/junos_commit_check/configs/rejects-generate-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-generate-v6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..8b39dc8 --- /dev/null +++ b/snapshots/junos_commit_check/configs/rejects-generate-v6/show_configuration_|_display_set.txt @@ -0,0 +1,2 @@ +set system host-name rejects-generate-v6 +set routing-options rib inet6.0 generate route 2001:db8::1/32 diff --git a/snapshots/junos_commit_check/configs/rejects-ospf3-area-range/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-ospf3-area-range/show_configuration_|_display_set.txt new file mode 100644 index 0000000..dd075db --- /dev/null +++ b/snapshots/junos_commit_check/configs/rejects-ospf3-area-range/show_configuration_|_display_set.txt @@ -0,0 +1,3 @@ +set system host-name rejects-ospf3-area-range +set protocols ospf3 area 0.0.0.0 interface lo0.0 +set protocols ospf3 area 0.0.0.0 area-range 2001:db8::1/32 diff --git a/snapshots/junos_commit_check/configs/rejects-static-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-static-v6/show_configuration_|_display_set.txt new file mode 100644 index 0000000..1218940 --- /dev/null +++ b/snapshots/junos_commit_check/configs/rejects-static-v6/show_configuration_|_display_set.txt @@ -0,0 +1,2 @@ +set system host-name rejects-static-v6 +set routing-options rib inet6.0 static route 2001:db8::1/32 reject diff --git a/snapshots/junos_commit_check/show/host_nos.txt b/snapshots/junos_commit_check/show/host_nos.txt index 41e3319..6d5edfc 100644 --- a/snapshots/junos_commit_check/show/host_nos.txt +++ b/snapshots/junos_commit_check/show/host_nos.txt @@ -6,10 +6,21 @@ "rejects-firewall-address": "junos", "rejects-firewall-next-ip": "junos", "rejects-condition": "junos", + "rejects-static-v6": "junos", + "rejects-aggregate-v6": "junos", + "rejects-generate-v6": "junos", + "rejects-ospf3-area-range": "junos", + "rejects-condition-v6": "junos", "accepts-prefix-list": "junos", "accepts-route-filter": "junos", "accepts-snmp": "junos", "accepts-bgp-allow": "junos", "accepts-interface": "junos", - "accepts-mpls-install": "junos" + "accepts-mpls-install": "junos", + "accepts-prefix-list-v6": "junos", + "accepts-route-filter-v6": "junos", + "accepts-interface-v6": "junos", + "accepts-bgp-allow-v6": "junos", + "accepts-firewall6-address": "junos", + "accepts-firewall6-next-ip6": "junos" } diff --git a/snapshots/junos_commit_check/validation/parse_warnings.yaml b/snapshots/junos_commit_check/validation/parse_warnings.yaml index 3253df8..f1661b7 100644 --- a/snapshots/junos_commit_check/validation/parse_warnings.yaml +++ b/snapshots/junos_commit_check/validation/parse_warnings.yaml @@ -12,7 +12,7 @@ # prefix normalization checks (batfish/batfish#9928). expects_fatal_warning: - # Junos rejects unnormalized prefixes in these contexts + # Junos rejects unnormalized prefixes in these contexts (IPv4) - host: rejects-static contains: "192.168.1.111/24" - host: rejects-aggregate @@ -27,12 +27,30 @@ expects_fatal_warning: contains: "192.168.1.111/24" - host: rejects-condition contains: "192.168.1.111/24" + # Junos rejects unnormalized prefixes in these contexts (IPv6) + - host: rejects-static-v6 + contains: "2001:db8::1/32" + - host: rejects-aggregate-v6 + contains: "2001:db8::1/32" + - host: rejects-generate-v6 + contains: "2001:db8::1/32" + - host: rejects-ospf3-area-range + contains: "2001:db8::1/32" + - host: rejects-condition-v6 + contains: "2001:db8::1/32" expects_no_fatal_warning: - # Junos accepts unnormalized prefixes in these contexts + # Junos accepts unnormalized prefixes in these contexts (IPv4) - accepts-prefix-list - accepts-route-filter - accepts-snmp - accepts-bgp-allow - accepts-interface - accepts-mpls-install + # Junos accepts unnormalized prefixes in these contexts (IPv6) + - accepts-prefix-list-v6 + - accepts-route-filter-v6 + - accepts-interface-v6 + - accepts-bgp-allow-v6 + - accepts-firewall6-address + - accepts-firewall6-next-ip6 diff --git a/snapshots/junos_commit_check/validation/sickbay.yaml b/snapshots/junos_commit_check/validation/sickbay.yaml index 521b0d0..3497ab8 100644 --- a/snapshots/junos_commit_check/validation/sickbay.yaml +++ b/snapshots/junos_commit_check/validation/sickbay.yaml @@ -26,3 +26,29 @@ entries: skip: skip_type: dont_run reason: "commit check lab: configs are parse-warning test fixtures, not full devices" + # IPv6 prefix normalization not yet implemented in Batfish + - hostname: rejects-static-v6 + test_name: test_parse_warnings + skip: + skip_type: xfail + reason: "https://github.com/batfish/batfish/issues/9934" + - hostname: rejects-aggregate-v6 + test_name: test_parse_warnings + skip: + skip_type: xfail + reason: "https://github.com/batfish/batfish/issues/9934" + - hostname: rejects-generate-v6 + test_name: test_parse_warnings + skip: + skip_type: xfail + reason: "https://github.com/batfish/batfish/issues/9934" + - hostname: rejects-ospf3-area-range + test_name: test_parse_warnings + skip: + skip_type: xfail + reason: "https://github.com/batfish/batfish/issues/9934" + - hostname: rejects-condition-v6 + test_name: test_parse_warnings + skip: + skip_type: xfail + reason: "https://github.com/batfish/batfish/issues/9934" diff --git a/src/lab_builder/validate.py b/src/lab_builder/validate.py index febd512..1361e3a 100644 --- a/src/lab_builder/validate.py +++ b/src/lab_builder/validate.py @@ -262,10 +262,15 @@ def _arista_check_bgp_peer(node: NodeInfo, neighbor: str) -> CheckResult: # --------------------------------------------------------------------------- -def _junos_commit_check(node: NodeInfo, config_lines: list[str]) -> tuple[bool, str]: +def _junos_commit_check( + node: NodeInfo, config_lines: list[str] +) -> tuple[bool | None, str]: """Load config lines on a Junos device and run 'commit check'. - Returns (success, output) where success is True if commit check passes. + Returns (result, output) where result is: + True - commit check succeeded + False - commit check was rejected (definitive failure output) + None - indeterminate (exception, timeout, or empty output) Always rolls back after the check so the device stays clean. """ from lab_builder.device import connect @@ -278,19 +283,28 @@ def _junos_commit_check(node: NodeInfo, config_lines: list[str]) -> tuple[bool, if not stripped or stripped.startswith("#"): continue conn.send_command_timing(stripped) - output: str = conn.send_command_timing("commit check") + output: str = conn.send_command( + "commit check", expect_string=r"#", read_timeout=30 + ) conn.send_command_timing("rollback 0") conn.exit_config_mode() - success = "configuration check succeeds" in output.lower() - return success, output.strip() + stripped_output = output.strip() + if "configuration check succeeds" in output.lower(): + return True, stripped_output + elif "error" in output.lower() or "failed" in output.lower(): + return False, stripped_output + elif not stripped_output: + return None, "empty output from commit check" + else: + return None, f"indeterminate output: {stripped_output}" except Exception as e: try: conn.send_command_timing("rollback 0") conn.exit_config_mode() except Exception: pass - return False, f"exception: {e}" + return None, f"exception: {e}" finally: conn.disconnect() @@ -299,8 +313,15 @@ def _check_commit_rejects( node: NodeInfo, config_lines: list[str], expected_error: str | None ) -> CheckResult: """Verify that Junos rejects the given config lines at commit check.""" - success, output = _junos_commit_check(node, config_lines) - if success: + result, output = _junos_commit_check(node, config_lines) + if result is None: + return CheckResult( + "commit_check_rejects", + node.name, + False, + f"indeterminate result: {output}", + ) + if result: return CheckResult( "commit_check_rejects", node.name, @@ -324,8 +345,15 @@ def _check_commit_rejects( def _check_commit_accepts(node: NodeInfo, config_lines: list[str]) -> CheckResult: """Verify that Junos accepts the given config lines at commit check.""" - success, output = _junos_commit_check(node, config_lines) - if success: + result, output = _junos_commit_check(node, config_lines) + if result is None: + return CheckResult( + "commit_check_accepts", + node.name, + False, + f"indeterminate result: {output}", + ) + if result: return CheckResult( "commit_check_accepts", node.name,