Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions infra/examples/junos-commit-check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Junos Commit Check Lab

Tests which Junos configuration syntax is rejected at commit time due to
unnormalized prefixes (host bits set). Results drive Batfish's
`fatalRedFlag` implementation for Junos prefix validation.

## Background

Junos performs "commit checks" that reject certain config at commit time
even though `set` commands accept it. For example, `set routing-options
static route 10.0.0.5/8 reject` is accepted by the CLI but rejected at
commit because the prefix has host bits set.

Batfish models this with `fatalRedFlag` warnings (see batfish/batfish#9928).
This lab empirically determines which `ip_prefix` grammar contexts enforce
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.

The `checks.yaml` uses two check types:

- `commit_check_rejects`: asserts `commit check` fails
- `commit_check_accepts`: asserts `commit check` succeeds

Each check loads config lines, runs `commit check`, and rolls back.

## Running

```bash
# 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"
```

## Results (vJunos 25.4R1.12)

**Rejects host bits:**

- `routing-options static route`
- `routing-options aggregate route`
- `routing-options generate route`
- `protocols ospf area X area-range`
- `firewall filter X term T from destination-address`
- `firewall filter X term T then next-ip`
- `policy-options condition X if-route-exists`

**Accepts host bits:**

- `policy-options prefix-list`
- `policy-options policy-statement X from route-filter`
- `snmp client-list`
- `protocols bgp group X allow`
- `protocols mpls label-switched-path X install`
- `interfaces X unit Y family inet address`
- `interfaces X unit Y family inet address A vrrp-group N track route`

**Not tested (requires vSRX):**

- `security nat` (pool address, match address, static-nat prefix)
- `security address-book`
- `security zones address-book`

## Corresponding Regression Test

The `snapshots/junos_commit_check/` snapshot contains one minimal config
per test case. The `test_parse_warnings` test in `lab_tests/test_labs.py`
asserts Batfish produces (or does not produce) fatal red flag warnings for
each, driven by `validation/parse_warnings.yaml`.
157 changes: 157 additions & 0 deletions infra/examples/junos-commit-check/checks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Junos commit check validation: tests whether Junos rejects unnormalized
# prefixes (host bits set) at commit time.
#
# Platform: vJunos-router (VMX). Security-hierarchy checks require vSRX.
# Reference: batfish/batfish#9928

checks:
# --- Contexts where Junos REJECTS host bits ---

- type: commit_check_rejects
node: dut
config_lines:
- "set routing-options static route 192.168.1.111/24 reject"
description: "static route with host bits set"

- type: commit_check_accepts
node: dut
config_lines:
- "set routing-options static route 192.168.1.0/24 reject"
description: "static route with valid prefix"

- type: commit_check_rejects
node: dut
config_lines:
- "set routing-options aggregate route 192.168.1.111/24"
description: "aggregate route with host bits set"

- type: commit_check_accepts
node: dut
config_lines:
- "set routing-options aggregate route 192.168.1.0/24"
description: "aggregate route with valid prefix"

- type: commit_check_rejects
node: dut
config_lines:
- "set routing-options generate route 192.168.1.111/24"
description: "generate route with host bits set"

- type: commit_check_accepts
node: dut
config_lines:
- "set routing-options generate route 192.168.1.0/24"
description: "generate route with valid prefix"

- type: commit_check_rejects
node: dut
config_lines:
- "set protocols ospf area 0.0.0.0 area-range 192.168.1.111/24"
description: "OSPF area-range with host bits set"

- type: commit_check_accepts
node: dut
config_lines:
- "set protocols ospf area 0.0.0.0 area-range 192.168.1.0/24"
description: "OSPF area-range with valid prefix"

- type: commit_check_rejects
node: dut
config_lines:
- "set firewall filter TEST term T from destination-address 192.168.1.111/24"
- "set firewall filter TEST term T then accept"
description: "firewall destination-address with host bits set"

- type: commit_check_accepts
node: dut
config_lines:
- "set firewall filter TEST term T from destination-address 192.168.1.0/24"
- "set firewall filter TEST term T then accept"
description: "firewall destination-address with valid prefix"

- type: commit_check_rejects
node: dut
config_lines:
- "set firewall filter TEST term T then next-ip 192.168.1.111/24"
description: "firewall next-ip with host bits set"

- type: commit_check_accepts
node: dut
config_lines:
- "set firewall filter TEST term T then next-ip 192.168.1.0/24"
description: "firewall next-ip with valid prefix"

- type: commit_check_rejects
node: dut
config_lines:
- "set policy-options condition TEST if-route-exists 192.168.1.111/24 table inet.0"
description: "condition if-route-exists with host bits set"

- type: commit_check_accepts
node: dut
config_lines:
- "set policy-options condition TEST if-route-exists 192.168.1.0/24 table inet.0"
description: "condition if-route-exists with valid prefix"

# --- Contexts where Junos ACCEPTS host bits ---

- type: commit_check_accepts
node: dut
config_lines:
- "set policy-options prefix-list TEST 192.168.1.111/24"
description: "prefix-list with host bits (accepted)"

- type: commit_check_accepts
node: dut
config_lines:
- "set policy-options policy-statement TEST term T from route-filter 192.168.1.111/24 exact"
description: "route-filter with host bits (accepted)"

- type: commit_check_accepts
node: dut
config_lines:
- "set policy-options policy-statement TEST term T from route-filter 10.0.0.0/8 through 192.168.1.111/24"
description: "route-filter through with host bits (accepted)"

- type: commit_check_accepts
node: dut
config_lines:
- "set snmp client-list TEST 192.168.1.111/24"
description: "SNMP client-list 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 192.168.1.111/24"
description: "BGP allow with host bits (accepted)"

- type: commit_check_accepts
node: dut
config_lines:
- "set protocols mpls label-switched-path TEST to 2.2.2.2"
- "set protocols mpls label-switched-path TEST install 192.168.1.111/24"
- "set protocols mpls interface ge-0/0/0.0"
description: "MPLS LSP install with host bits (accepted)"

- type: commit_check_accepts
node: dut
config_lines:
- "set interfaces ge-0/0/1 unit 0 family inet address 192.168.1.111/24"
description: "interface address with host bits (accepted)"

- type: commit_check_accepts
node: dut
config_lines:
- "set interfaces ge-0/0/0 unit 0 family inet address 10.0.0.1/30 vrrp-group 1 virtual-address 10.0.0.2"
- "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)"

# TODO: Security hierarchy checks (require vSRX image)
# - security nat source pool <p> address <prefix>
# - security nat source pool <p> address <from> to <to>
# - security nat rule match destination-address / source-address
# - security nat static rule then static-nat prefix
# - security address-book address
# - security zones security-zone address-book address
31 changes: 31 additions & 0 deletions infra/examples/junos-commit-check/configs/dut.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
system {
host-name dut;
}
interfaces {
ge-0/0/0 {
unit 0 {
family inet {
address 10.0.0.1/30;
}
}
}
lo0 {
unit 0 {
family inet {
address 1.1.1.1/32;
}
}
}
}
routing-options {
autonomous-system 65000;
router-id 1.1.1.1;
}
protocols {
ospf {
area 0.0.0.0 {
interface lo0.0;
interface ge-0/0/0.0;
}
}
}
14 changes: 14 additions & 0 deletions infra/examples/junos-commit-check/topology.clab.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Junos commit check validation lab: tests which config syntax Junos
# rejects at commit time (prefix normalization, etc.)
#
# Single node — no links needed. We just need a running Junos to SSH into
# and test commit checks against.

name: junos-commit-check

topology:
nodes:
dut:
kind: juniper_vjunosrouter
image: vrnetlab/juniper_vjunos-router:25.4R1.12
startup-config: configs/dut.cfg
88 changes: 88 additions & 0 deletions lab_tests/test_labs.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,94 @@ def test_vi_model(bf: Session, snapshot: str) -> None:
bf.q.viModel().answer()


@pytest.fixture(scope="module")
def parse_warning_spec(pytestconfig: Config) -> dict | None:
"""Load parse_warnings.yaml if present."""
import yaml

lab = pytestconfig.getoption(LAB_NAME_CONFIG_OPTION)
warnings_path = snapshot_path(lab) / "validation" / "parse_warnings.yaml"
if not warnings_path.exists():
return None
with open(warnings_path) as f:
return yaml.safe_load(f)


@pytest.fixture(scope="module")
def host_fatal_details(
bf: Session, snapshot: str, parse_warning_spec: dict | None
) -> dict[str, list[str]]:
"""Map hostname -> list of fatal red flag warning detail strings."""
if parse_warning_spec is None:
return {}

bf.set_snapshot(snapshot)
issues = bf.q.initIssues().answer().frame()

fatal_rows = issues[
(issues["Type"] == "Parse warning (redflag)")
& (issues["Details"].str.startswith("FATAL:"))
]

result: dict[str, list[str]] = {}
for _, row in fatal_rows.iterrows():
nodes = row.get("Nodes")
source_lines = row.get("Source_Lines")
if nodes:
for node in nodes:
result.setdefault(node, []).append(row["Details"])
elif source_lines:
for file_lines in source_lines:
filename = (
file_lines.filename
if hasattr(file_lines, "filename")
else str(file_lines)
)
parts = filename.split("/")
if len(parts) >= 2 and parts[0] == "configs":
result.setdefault(parts[1], []).append(row["Details"])
return result


def test_parse_warnings(
hostname: str,
vendor: Vendor,
parse_warning_spec: dict | None,
host_fatal_details: dict[str, list[str]],
) -> None:
"""Tests that Batfish produces (or does not produce) fatal red flag warnings.

Driven by validation/parse_warnings.yaml. Each host is tested individually
so failures can be sickbayed per-host.
"""
if parse_warning_spec is None:
pytest.skip("no validation/parse_warnings.yaml")
return

expects_fatal = {
e["host"]: e["contains"]
for e in parse_warning_spec.get("expects_fatal_warning", [])
}
expects_no_fatal = set(parse_warning_spec.get("expects_no_fatal_warning", []))

if hostname in expects_fatal:
contains = expects_fatal[hostname]
details = host_fatal_details.get(hostname, [])
if not any(contains in d for d in details):
raise ValidationError(
f"expected fatal warning containing '{contains}' for '{hostname}' "
f"but got: {details}"
)
elif hostname in expects_no_fatal:
if hostname in host_fatal_details:
raise ValidationError(
f"unexpected fatal warning for '{hostname}': "
f"{host_fatal_details[hostname]}"
)
else:
pytest.skip(f"'{hostname}' not in parse_warnings.yaml")


# TODO: Re-enable when reachability verification logic is ported from Batfish
# def test_reachability_verifier(bf: Session, snapshot: str) -> None:
# """Tests that the Reachability Verifier finds no bugs."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
set system host-name accepts-bgp-allow
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 192.168.1.111/24
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set system host-name accepts-interface
set interfaces ge-0/0/0 unit 0 family inet address 192.168.1.111/24
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
set system host-name accepts-mpls-install
set protocols mpls label-switched-path TEST to 2.2.2.2
set protocols mpls label-switched-path TEST install 192.168.1.111/24
set protocols mpls interface ge-0/0/0.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set system host-name accepts-prefix-list
set policy-options prefix-list TEST 192.168.1.111/24
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set system host-name accepts-route-filter
set policy-options policy-statement TEST term T from route-filter 192.168.1.111/24 exact
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set system host-name accepts-snmp
set snmp client-list TEST 192.168.1.111/24
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
set system host-name rejects-aggregate
set routing-options aggregate route 192.168.1.111/24
Loading
Loading