# Batfish: Network Validation Before You Break Production

Every article so far has been about doing something to the network: pushing configurations, collecting state, reacting to events. This article is about the step that happens before any of that: checking whether a change is safe before it ever touches a device.

That's the job **Batfish** was built for. It's a static analysis engine for network configuration: it parses your config files, builds a vendor-neutral model of the network (RIBs, FIBs, ACLs, BGP sessions, the works), and lets you ask questions like **"will this ACL block traffic it shouldn't"** or **"is this BGP session actually going to come up"** without touching a single live device. **No SSH**, **no** **NETCONF** session, no risk of a bad commit taking down a production network

# Batfish in a Multivendor Network

One reason Batfish is interesting for network engineers is its broad platform support.

The current Batfish project lists support for platforms including Cisco IOS, IOS-XE, IOS-XR, NX-OS, Juniper JunOS, Arista EOS, Palo Alto, Fortinet, SONiC, and several others. **Nokia SR OS was not supported on Batfish.** Only recently *Nokia SR OS / SR-SIM* with MD-CLI is supported.

However, "supported platform" does **not** mean every feature of every software release is perfectly modeled.

Batfish validation work continues to uncover vendor-specific behavioral differences, and current issue reports show some features may be partially recognized or modeled differently from real device behavior

For production use, validate the specific features your network depends on.

# Why bother with this on top of pyATS/Genie and NAPALM

Worth being precise about what Batfish is *not.* It's not a **live-state** collector like NAPALM or Genie, and it's not a **config-push** tool like Netmiko or Ansible. It doesn't care what the device is doing right now. It cares what the device is **configured to do**, and what will actually happen once that configuration is active. That distinction matters:

Genie's `learn()` parses live show-command output. It displays the current operational state as documented in our earlier articles.

Batfish parses the configuration itself and computes forwarding behavior from first principles. It doesn't need the device to be ***reachable*** or ***even up***. A snapshot of the configuration file is ***enough***.

# Setup

## **Installing Batfish and Pybatfish**

Getting started with Batfish is easy. First, pull and run the latest Docker container:

```python
docker pull batfish/allinone:test-2026.08.20.3666
docker run -d --name batfish \
  -v batfish-data:/data \
  -p 8888:8888 -p 9997:9997 -p 9996:9996 \
  batfish/allinone:test-2026.08.20.3666
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><em>(Substitute whatever the current newest </em><code>test-*</code><em> tag is by the time you're reading this: they roll frequently.)</em></div>
</div>

Confirm the container actually started before moving on

```python
docker ps --filter name=batfish
```

Then, install Pybatfish using `pip`:

```python
python3 -m venv batfish-env
source batfish-env/bin/activate
pip install --upgrade pybatfish
```

Pybatfish requires Python 3, and it is recommend that you install it in a **virtual environment**

## Topology

Our test bed is powered by [containerlab](https://containerlab.dev/)

![](https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/88a78001-8497-4933-86c2-7c6363c05eb1.jpg align="center")

## Collecting SR OS configuration for a snapshot

Batfish snapshots are just directories of config files on a disk; no live connectivity required for the analysis itself.

**SFTP** pulling a saved config file is reliable and the easiest route

On each node, save the running config to a file first:

```python
# From MD-CLI, in the router's own session
admin save cf3:\PE1.cfg
```

Then pull it off with SFTP:

```python
mkdir -p snapshot/configs
sftp admin@PE1:cf3:/PE1.cfg snapshot/configs/PE1.cfg
```

**Directory layout**: Batfish expects the following structure, with configs/ holding device configurations

```python
# sample of the snapshot directory structure 
snapshot/
└── configs/
    ├── PE1.cfg
    ├── PE2.cfg
    └── switch-1.cfg
    
```

# Interacting with the Batfish Service

## Batfish Questions

Batfish builds a network model from your configs and other data. You can then query that model or the parsed config settings directly using several categories of Batfish questions.

Before we start, always check the `File Parse Status` of your saved configurations. I faced an issue in which the Batfish format detector didn't recognize the files as any supported vendor.

**The actual cause**: The `batfish/allinone:latest` Docker image didn't have the Nokia SR OS parser.

## **Getting status of parsed files**

Batfish may ignore certain lines in the configuration. To retrieve the parsing status of snapshot files, use the `fileParseStatus()` question.

**Invocation :**

```python
from pybatfish.client.session import Session
from pybatfish.datamodel import *
bf = Session(host="localhost")
bf.set_network("multicast-env")
bf.init_snapshot("snapshot", name="multicast", overwrite=True)
bf.q.fileParseStatus().answer().frame()
```

**Output** :

```python
         File_Name  Status  File_Format    Nodes
0  configs/CE1.cfg  PASSED  NOKIA_SROS    ['switch-1']
1  configs/CE2.cfg  PASSED  NOKIA_SROS    ['switch-2']
2  configs/CE3.cfg  PASSED  NOKIA_SROS    ['switch-3']
3  configs/PE1.cfg  PASSED  NOKIA_SROS    ['pe1']
4  configs/PE2.cfg  PASSED  NOKIA_SROS    ['pe2']
```

## **Interface Properties**

Returns configuration settings of interfaces; `nodes="pe1"` is a filter narrowing the question to one node's rows, not a sign that pe1 is its own snapshot. Drop the filter (or use `nodes=`".\*" ) to see every router in the snapshot at once.

**Invocation :**

```python
interfaces =bf.q.interfaceProperties(nodes="pe1").answer().frame()
```

**Output :**

```python
                Interface  Active Admin_Up        All_Prefixes
0         pe1["1/1/c1/1"]  True     True                  []                          
1         pe1["1/1/c3/1"]  True     True                  []                          
2  pe1[ng-mvpn-ldp.toCE1]  True     True  ['172.28.11.5/24']                      
3  pe1[ng-mvpn-ldp.toCE3]  True     True  ['172.28.13.5/24']                        
4           pe1[switch-1]  True     True   ['172.28.1.1/27']                          
5           pe1[switch-2]  True     True   ['172.28.2.1/27']                          
6           pe1[switch-3]  True     True   ['172.28.3.1/27']                           
7             pe1[system]  True     True  ['192.10.10.1/32']                            
8              pe1[toPE2]  True     True   ['192.10.1.2/24'] 

--------- output Truncated 
```

Or just pull the columns that actually matter day-to-day:

```python
interfaces =bf.q.interfaceProperties(nodes="pe1").answer().frame()
interfaces[["Interface", "Primary_Address", "Active", "VRF", "MTU"]]
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Active: True</strong> here doesn't mean "this interface is up right now". Batfish has no live connection to the device. It's a purely static-analysis conclusion drawn from the configuration alone.</div>
</div>

## **BGP Peer Configuration**

Returns configuration settings for BGP peerings. Reports configuration settings for each configured BGP peering on each node in the network. This question reports peer-specific settings. Settings that are process-wide are reported by the `bgpProcessConfiguration` question.

**Invocation :**

```python
peers = bf.q.bgpPeerConfiguration(nodes="pe1").answer().frame()
peers[["Local_AS", "Remote_AS", "Remote_IP", "Local_IP"]]
```

**Output :**

```python
  Local_AS Remote_AS    Remote_IP Local_IP
0    65000     65000  192.10.10.2     None
1    65000     65001  172.28.13.4     None
2    65000     65001  172.28.11.4     None
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">An incorrect AS on one PE is invisible in isolation but obvious the moment two nodes' <code>bgpPeerConfiguration</code> outputs are compared side by side. A mismatch shows up fast across a multi-PE snapshot by pulling the same question with nodes=".*" and diff <code>Remote_AS</code></div>
</div>

## **IP Owners**

Returns where IP addresses are attached in the network. For each device, lists the mapping from IPs to corresponding interface(s) and VRF(s)

**Invocation :**

```python
result = bf.q.ipOwners(nodes="pe1").answer().frame()
result.head(5)
```

**Output :**

```python
       Node          VRF          Interface           IP   Mask 
0       pe1      default              toPE2   192.10.1.2   24   
1  switch-1      default             system  192.10.10.3   32  
2       pe1      default           switch-3   172.28.3.1   27   
3       pe2  ng-mvpn-ldp  ng-mvpn-ldp.toCE3  172.28.14.5   24   T
4  switch-2  ng-mvpn-ldp  ng-mvpn-ldp.toPE2  172.28.12.4   24   
```

## **Routing and Forwarding Tables**

This category of questions allows you to query the RIBs and FIBs computed by Batfish.

**Invocation :**

```python
routes = bf.q.routes().answer().frame()
routes[routes["Node"] == "pe1"]
```

**Output :**

```python
  Node          VRF         Network                     Next_Hop  
0  pe1      default   172.28.1.0/27           interface switch-1  
1  pe1      default   172.28.2.0/27           interface switch-2  
2  pe1      default   172.28.3.0/27           interface switch-3  
3  pe1      default   192.10.1.0/24              interface toPE2  
4  pe1      default  192.10.10.1/32             interface system  
5  pe1      default  192.10.10.2/32                ip 192.10.1.1     
6  pe1  ng-mvpn-ldp  172.28.11.0/24  interface ng-mvpn-ldp.toCE1  
7  pe1  ng-mvpn-ldp  172.28.13.0/24  interface ng-mvpn-ldp.toCE3

........ output truncated 
```

## **Traceroute**

Performs a virtual traceroute in the network from a starting node. A destination IP and ingress (source) node must be specified. Other IP headers are given default values if unspecified. Unlike a real traceroute, this traceroute is directional. That is, for it to succeed, the reverse connectivity is not needed. This feature can help debug connectivity issues by decoupling the two directions.

**Invocation :**

```python
result = bf.q.traceroute(
    startLocation="pe1",
    headers=HeaderConstraints(dstIps="192.10.10.2")
).answer().frame()
```

**Output :**

```python

                                                Flow                                             
0  start=pe1 [172.28.1.1:49152->192.10.10.2:33434...   FORWARDED
1  start=pe1 [172.28.2.1:49152->192.10.10.2:33434...   FORWARDED
2  start=pe1 [172.28.3.1:49152->192.10.10.2:33434...   FORWARDED
3  start=pe1 [192.10.1.2:49152->192.10.10.2:33434...   FORWARDED
4  start=pe1 [192.10.10.1:49152->192.10.10.2:3343...   FORWARDED 
5  start=pe1 vrf=ng-mvpn-ldp [172.28.11.5:49152->...   NO_ROUTE
6  start=pe1 vrf=ng-mvpn-ldp [172.28.13.5:49152->...   NO_ROUTE

......... output truncated         
```

> This is the one that tends to sell people on Batfish. It simulates a flow through the modeled network and shows every hop, including ones a live traceroute would never reveal (like a policy silently dropping the packet two hops earlier than expected):

## **Reachability**

Searches across all flows that match the specified conditions and returns examples of such flows. This question can be used to ensure that certain services are globally accessible and parts of the network are perfectly isolated from each other.

**Invocation :**

```python
bf.q.reachability(
    pathConstraints=PathConstraints(startLocation="pe1", endLocation="pe2"),
    headers=HeaderConstraints(dstIps="192.10.10.2/32")
).answer().frame()
```

**Output :**

```python
0  start=pe1 [172.28.1.1->192.10.10.2 ICMP (type=...  [((ORIGINATED(default), FORWARDED(Forwarded ou...          1
```

# Final Thoughts

Everything above only scratches **one corner** of what Batfish can actually do. The questions covered here are parse status, BGP session/RIB checks, traceroute; these are the ones that mapped directly onto the SR OS troubleshooting done in this article, but Batfish's question library is much larger, and different use cases lean on entirely different parts of it:

**Pre-change validation / CI merge gates**: the pattern built out here: diff a candidate snapshot against a baseline before a config ever reaches a device

**Security and compliance auditing**: checking every ACL/firewall filter across the network at once for mistakes: rules that never apply, rules that are too permissive, or filters missing where they're expected

**End-to-end reachability and blast-radius analysis**: *"if this link/node fails, what's still reachable"* or *"can subnet A reach subnet B under any circumstance,"* computed once for the whole network rather than traced link-by-link

**Design/architecture review before a build**: modeling a planned topology from draft configs before any hardware or lab is stood up, to sanity-check routing and reachability on paper first

**One article's worth of examples doesn't come close to covering all use cases.** Everything demonstrated here was scoped narrowly to what came up while troubleshooting one lab topology.

**Where it actually sits in the automation stack:** Batfish isn't a replacement for anything already built in this series. It's the layer that runs ***before*** all of it. A realistic pipeline would look like this :

```python
config change (git commit)
        │
        ▼
   Batfish validation   ← this article: parse status,
   (pre-deployment)       ACL/BGP sanity checks against a snapshot
        │  (pass)
        ▼
   Netmiko/pySROS push   ← router-backups-pipeline, pysros script
   (deployment)
        │
        ▼
   Ansible EDA reaction   ← ansible-eda-sros:event-driven resose
   (post-deploy events)    to things like the CHASSIS cardFailure 
        │
        ▼
   pyATS/Genie / gNMIc    ← live operational-state verification,
   telemetry checks         clab-monitoring-stack dashboards
   (post-deployment)
```
