Skip to main content

This solution leverages Forward Networks as the centralized platform that parses and indexes configuration files, making it easier for network teams to use and trust the validation process. While simple syntax checks can be replicated using other tools, the centralized nature of this solution ensures consistency and ease of use, integrating seamlessly into existing workflows.

 

Overview

Network outages can be a significant issue for organizations, often caused by misconfigurations in routing protocols such as Border Gateway Protocol (BGP). To mitigate these outages, we created a Network Query Engine (NQE) script to validate route map configurations in a sandbox environment before they are applied to an actual device. 

Results

  • The primary goal of the query is to prevent network outages by identifying misconfigurations in BGP route maps. Misconfigurations, such as incorrectly permitting all routes or referencing non-existent objects such as access-lists or prefix-lists, have previously led to multiple network outages. This script catches those errors before they could impact the network.
  • The query was designed to detect these errors by staging changes in a sandbox environment where configurations can be edited and the syntax verified before deployment. This preemptive approach allows network administrators to identify and rectify potential issues that could cause outages, thereby enhancing network stability and reliability.

Solution

The solution revolves around using the NQE to analyze route map configurations in a sandbox environment. The NQE performs a series of checks to identify common misconfigurations.  If a route map contains a line that permits all routes followed by additional logic, the query flags this as an error. This is because such a configuration would not logically proceed past the permissive entry, potentially causing unintended behavior. The query also checks for route maps that reference access lists, prefix lists, or community lists that do not exist. This ensures that configurations are referring to valid objects, preventing unexpected routing behaviors. By integrating these checks into their workflow, network teams can stage their changes in a sandbox, run the NQE to highlight issues, and then review and approve the changes with confidence that they will not cause outages. This process involves:

  1. Staging Configurations: Using the sandbox feature, network administrators can stage candidate configurations in a controlled environment.
  2. Running the Query: The NQE analyzes the staged configurations, identifying any misconfigurations based on predefined rules.
  3. Review and Approval: Once the query flags potential issues, administrators can review and rectify them before deploying the configurations to the live environment.
/**
* @intent Perform syntax validation on route-maps
* @description
* Checks Performed:
* 1) If Object is referenced (ACL, as-path acl, community-list, prefix-list) then is that object defined somewhere in the config
* 2) Does a "match all" route-map statement (e.g., route-map sequence without a match statement above route-map )
* 3) The following objects are not checked
* mac-list - this is only used for an obsolete technology (OTV)
* ospf-area - value defined in-line and not via a referenced object
* route-type - value defined in-line and not via a referenced object
* source-protocol - value defined in-line and not via a referenced object
* multicast group - value defined in-line and not via a referenced object
* 4) Supported OS:
* IOS, IOS XE, NXOS
* 5) Disclaimer - This query checks for allowed syntax of ANY of the supported versions. If an NXOS supported command is drafted on an IOS-XE device, this query will return violation:false
*
*/

//Utility Functions
export getDeviceConfig(device: Device) =
max(foreach command in device.outputs.commands
where command.commandType == CommandType.CONFIG
select command.response);

startsWith(string, searchString) =
substring(string, 0, length(searchString)) == searchString;

getIpVer(ipVer) =
if !(isPresent(ipVer.left) || isPresent(ipVer.right)) || isPresent(ipVer.left)
then 4
else 6;

getObjectList(listOfRecords) =
foreach item in listOfRecords
let name = item.name
let ipVer = item?.ipVer
select distinct { ipVer, name };

getObjectReferences(type,
patternName,
matches,
ipVer,
excludedObjects,
definedObjects) =
foreach match in imatches]
let objectList = if isPresent(excludedObjects)
then (foreach object in match.objectList
where object not in excludedObjects
select object)
else (foreach object in match.objectList
select object)
foreach object in objectList
let name = match.name
select {
name,
seq: match.seq,
action: match.action,
objectType: type,
pattern: patternName,
objectName: object,
isDefined: { ipVer, name: object } in definedObjects
};

getModifiedConfig(device) =
max(foreach x in i0]
let config = getDeviceConfig(device)
where isPresent(config)
let config = replace(config, "\n", "\n ")
let config = replace(config, "\n !", "\n!")
let config = replace(config, "\n route-map", "\nroute-map")
let config = replace(config, "\n vrf", "\nvrf")
let config = replace(config, "\n udld", "\nudld")
let config = replace(config, "\n service", "\nservice")
let config = replace(config, "\n vlan", "\nvlan")
let config = replace(config, "\n ip", "\nip")
let config = replace(config, "\n port-profile", "\nport-profile")
let config = replace(config, "\n key", "\nkey")
let config = replace(config, "\n logging", "\nlogging")
let config = replace(config, "\n interface", "\ninterface")
let config = parseConfigBlocks(OS.OTHER, config)
select config);

//Route Map Definitions and Usages
patternDefinition = ```
route-map {name:string}
```;

// Exclude "description" since the text that follows could include "route-map".
patternLevel0 =
```
{!"description"} {(!"route-map" string)+} route-map {name:string}
```;

patternLevel1 =
```
{string}
{!"description"} {(!"route-map" string)+} route-map {name:string}
```;

patternLevel2 =
```
{string}
{string}
{!"description"} {(!"route-map" string)+} route-map {name:string}
```;

patternLevel3 =
```
{string}
{string}
{string}
{!"description"} {(!"route-map" string)+} route-map {name:string}
```;

patternLevel4 =
```
{string}
{string}
{string}
{string}
{!"description"} {(!"route-map" string)+} route-map {name:string}
```;

getRouteMapNames(config, pattern) =
foreach match in blockMatches(config, pattern)
select match.data.name;

getRouteMapUsages(config) =
foreach pattern
in ipatternLevel0,
patternLevel1,
patternLevel2,
patternLevel3,
patternLevel4
]
foreach name in getRouteMapNames(config, pattern)
select distinct name;

//ACL Patterns and Functions

aclDef1 = ```
ip access-list {string} {aclName:string}
```;

aclDef2 = ```
{ipVer:("ip" | "ipv6")} access-list {aclName:string} {eof}
```;

aclDef3 = ```
access-list {aclName:string} {string}
```;

getAclDefinitions(config) =
max(foreach x in i0]
let aclDefinitions1 = (foreach match in blockMatches(config, aclDef1)
select { ipVer: 4, name: match.data.aclName })
let aclDefinitions2 = (foreach match in blockMatches(config, aclDef2)
select {
ipVer: getIpVer(match.data.ipVer),
name: match.data.aclName
})
let aclDefinitions3 = (foreach match in blockMatches(config, aclDef3)
select { ipVer: 4, name: match.data.aclName })
select distinct getObjectList(aclDefinitions1 + aclDefinitions2 +
aclDefinitions3));

rmAcl1 =
```
route-map {name:string} {action:string} {seq:number}
match {ipVer:("ip" | "ipv6")} address {objectList:(!"prefix-list" string*)}
```;

rmAcl2 =
```
route-map {name:string} {action:string} {seq:number}
match {ipVer:("ip" | "ipv6")} {type:("route-source" | "next-hop")} {objectList:(!"prefix-list" string*)}
```;

getAclReferences(config, definedAcls) =
max(foreach x in i0]
let rmAcl1Matches = (foreach match in blockMatches(config, rmAcl1)
let matchArg = match.data
let ipVerArg = getIpVer(match.data.ipVer)
foreach object
in getObjectReferences("acl",
"rmAcl1",
matchArg,
ipVerArg,
null : List<String>,
definedAcls)
select object)
let rmAcl2Matches = (foreach match in blockMatches(config, rmAcl2)
let matchArg = match.data
let ipVerArg = getIpVer(match.data.ipVer)
foreach object
in getObjectReferences("acl",
"rmAcl2",
matchArg,
ipVerArg,
null : List<String>,
definedAcls)
select object)
select rmAcl1Matches + rmAcl2Matches);

//Prefix-List Patterns and Functions
plDef1 = ```
{ipVer:("ip" | "ipv6")} prefix-list {plName:string}
```;

plDef2 = ```
{plName:string} seq {number} {string} {ipv4Subnet}
```;

getPlDefinitions(config) =
max(foreach x in i0]
let plDefinitions1 = (foreach match in blockMatches(config, plDef1)
select {
ipVer: getIpVer(match.data.ipVer),
name: match.data.plName
})
let plDefinitions2 = (foreach match in blockMatches(config, plDef2)
select { ipVer: 4, name: match.data.plName })
select distinct getObjectList(plDefinitions1 + plDefinitions2));

rmPl1 =
```
route-map {name:string} {action:string} {seq:number}
match {ipVer:("ip" | "ipv6")} address prefix-list {objectList:(string*)}
```;

rmPl2 =
```
route-map {name:string} {action:string} {seq:number}
match {ipVer:("ip" | "ipv6")} {type:("route-source" | "next-hop")} prefix-list {objectList:(string*)}
```;

rmPl3 =
```
route-map {name:string} {action:string} {seq:number}
{statement:(!"match" !"set" !"description" !"continue" string)} {statement2:(string*)}
```;

getPlReferences(config, definedPls) =
max(foreach x in i0]
let rmPl1Matches = (foreach match in blockMatches(config, rmPl1)
let matchArg = match.data
let ipVerArg = getIpVer(match.data.ipVer)
foreach object
in getObjectReferences("prefix-list",
"rmPl1",
matchArg,
ipVerArg,
null : List<String>,
definedPls)
select object)
let rmPl2Matches = (foreach match in blockMatches(config, rmPl2)
let matchArg = match.data
let ipVerArg = getIpVer(match.data.ipVer)
foreach object
in getObjectReferences("prefix-list",
"rmPl2",
matchArg,
ipVerArg,
null : List<String>,
definedPls)
select object)
let rmPl3Matches = (foreach match in blockMatches(config, rmPl3)
let matchArg = { name: match.data.name,
action: match.data.action,
seq: match.data.seq,
ipver: { left: null : {},
right: null : {}
},
objectList: tmatch.data.statement] +
match.data.statement2
}
let ipVerArg = 4
foreach object
in getObjectReferences("prefix-list",
"rmPl3",
matchArg,
ipVerArg,
null : List<String>,
definedPls)
select object)
select rmPl1Matches + rmPl2Matches + rmPl3Matches);

//Route-Maps that match Community-Lists
clDef1 = ```
ip community-list {string} {clName:string}
```;

clDef2 = ```
ip community-list {clName:(!"expanded" !"standard" string)}
```;

getclDefinitions(config) =
max(foreach x in i0]
let clDefinitions1 = (foreach match in blockMatches(config, clDef1)
select { ipVer: 0, name: match.data.clName })
let clDefinitions2 = (foreach match in blockMatches(config, clDef2)
select { ipVer: 0, name: match.data.clName })
select distinct getObjectList(clDefinitions1 + clDefinitions2));

rmCl1 =
```
route-map {name:string} {action:string} {seq:number}
match community {objectList:(string*)}
```;

getClReferences(config, definedCls) =
max(foreach x in i0]
let rmCl1Matches = (foreach match in blockMatches(config, rmCl1)
let matchArg = match.data
let ipVerArg = 0 //Match IPv4 and IPv6
foreach object
in getObjectReferences("community",
"rmCl1",
matchArg,
ipVerArg,
"exact-match"],
definedCls)
select object)
select rmCl1Matches);

//Route-Maps that match AS-Path Access-Lists
asPathDef1 = ```
ip as-path access-list {asPathName:string}
```;

getasPathDefinitions(config) =
max(foreach x in i0]
let asPathDefinitions1 = (foreach match
in blockMatches(config, asPathDef1)
select { ipVer: 0, name: match.data.asPathName })
select distinct getObjectList(asPathDefinitions1));

rmAsPath1 =
```
route-map {name:string} {action:string} {seq:number}
match as-path {objectList:(string*)}
```;

getAsPathReferences(config, definedAsPathAcls) =
max(foreach x in i0]
let rmAsPathMatches = (foreach match in blockMatches(config, rmAsPath1)
let matchArg = match.data
let ipVerArg = 0 //IPv4 and IPv6
foreach object
in getObjectReferences("as-path",
"rmAsPath1",
matchArg,
ipVerArg,
null : List<String>,
definedAsPathAcls)
select object)
select rmAsPathMatches);

//Match Statements that aren't verified
noCheck1 =
```
route-map {name:string} {action:string} {seq:number}
match {matchSyntax:(string*)}
```;

getUnchecked(config) =
max(foreach x in i0]
let uncheckedMatch = (foreach match in blockMatches(config, noCheck1)
let syntax = join(" ", match.data.matchSyntax)
let isChecked = isChecked(syntax)
where !isPresent(isChecked) || !isChecked
select {
name: match.data.name,
seq: match.data.seq,
action: match.data.action,
objectType: "unchecked",
pattern: "none",
objectName: syntax,
isDefined: true
})
select uncheckedMatch);

checkedSyntax =
>"ip address",
"as-path",
"community",
"ip route-source",
"ip next-hop",
"ipv6 address",
"ipv6 route-source",
"ipv6 next-hop"
];

isChecked(syntax) =
max(foreach item in checkedSyntax
where length(syntax) > length(item)
select startsWith(syntax, item));

rmNoSeqPattern1 =
```
route-map {name:string} {!"pbr-statistics"} {string} {eof}
```;

rmNoSeqPattern2 = ```
route-map {name:string} {eof}
```;

getNoSeq(config) =
max(foreach x in i0]
let rmNoSeqMatches1 = (foreach match
in blockMatches(config, rmNoSeqPattern1)
select {
name: match.data.name,
seq: null : Number,
action: null : String,
objectType: "no route-map sequence",
pattern: "rmNoSeqPattern1",
objectName: null : String,
isDefined: false
})
let rmNoSeqMatches2 = (foreach match
in blockMatches(config, rmNoSeqPattern2)
select {
name: match.data.name,
seq: null : Number,
action: null : String,
objectType: "no route-map sequence",
pattern: "rmNoSeqPattern2",
objectName: null : String,
isDefined: false
})
select rmNoSeqMatches1 + rmNoSeqMatches2);

//Sequences with No Match Statement
rmSeqNoMatchPattern1 =
```
route-map {name:string} {action:string} {seq:number}
{%-%} match
```;

getRouteMapSeqNoMatch(config) =
foreach match in blockMatches(config, rmSeqNoMatchPattern1)
where match.data.action != "pbr-statistics"
select {
name: match.data.name,
seq: match.data.seq,
action: match.data.action,
objectType: "no match",
pattern: "rmSeqNoMatchPattern1",
objectName: null : String,
isDefined: true
};

//Match All Routemap Sequences
rmPattern1 =
```
route-map {name:string} {action:string} {seq:(number | empty)}
```;

rmPattern2 = ```
route-map {name:string} {eof}
```;

getpartialRouteMapSequences(config) =
foreach match in blockMatches(config, rmPattern2)
select { name: match.data.name, action: null : String, seq: null : Number };

getRouteMapSequences(config) =
foreach match in blockMatches(config, rmPattern1)
select {
name: match.data.name, action: match.data.action, seq: match.data.seq?.left
};

getAllRouteMapSequences(config) =
getpartialRouteMapSequences(config) + getRouteMapSequences(config);

validMatchCriteriaPrefix =
>"ip next-hop",
"metric",
"interface",
"metric",
"next-hop",
"tag",
"route-type",
"ip multicast group",
"mac-list",
"source-protocol",
"ospf-area"
];
//https://cisco.com/c/en/us/td/docs/routers/ios/config/17-x/ip-routing/b-ip-routing/m_iri-iprouting.html

notCheckedSyntaxCheck(string) =
length(foreach criteria in validMatchCriteriaPrefix
where length(criteria) < length(string)
where startsWith(string, criteria)
select criteria) >
0;

foreach device in network.devices
let rmConfig = getModifiedConfig(device)
where isPresent(rmConfig)
let config = device.files.config
where isPresent(config)
let definedPls = getPlDefinitions(config)
let referencedPls = getPlReferences(rmConfig, definedPls)
let referencedPlsForNoMatch = (foreach pl in referencedPls
select {
name: pl.name, seq: pl.seq, action: pl.action
})
let definedCls = getclDefinitions(config)
let referencedCls = getClReferences(config, definedCls)
let definedAsPathAcls = getasPathDefinitions(config)
let referencedAsPathAcls = getAsPathReferences(config, definedAsPathAcls)
let definedAcls = getAclDefinitions(config)
let referencedAcls = getAclReferences(config, definedAcls)
let uncheckedReferences = getUnchecked(config)
let noMatchSequences = getRouteMapSeqNoMatch(config)
let noSeq = getNoSeq(config)
let allRouteMapSequences = getAllRouteMapSequences(config)
let routeMapUsages = getRouteMapUsages(config)
foreach match in allRouteMapSequences
let routeMap = match.name
let seq = match.seq
let maxSeq = max(foreach rm in allRouteMapSequences
where rm.name == routeMap
select rm.seq)
let numSequences = length(foreach rm in allRouteMapSequences
where rm.name == routeMap && rm.seq == seq
select rm)
foreach object
in referencedAcls + referencedPls + referencedCls + referencedAsPathAcls +
noMatchSequences +
uncheckedReferences +
noSeq
where object.name == routeMap && seq == object.seq
let noMatchViolation = seq != maxSeq && object.objectType == "no match"
where !(noMatchViolation &&
{ name: object.name, seq: object.seq, action: object.action } in
referencedPlsForNoMatch)
let result = "Passed"
let result = if object.isDefined && !noMatchViolation
then result
else if object.objectType == "no route-map sequence"
then "Violation - No Route-Map Sequence"
else if noMatchViolation
then "Violation - No Match Statement"
else "Violation - Object Not Defined"
let result = if object.objectType == "unchecked" then "Not Checked" else result
let result = if result == "Not Checked"
then if notCheckedSyntaxCheck(object.objectName)
then "Not Checked"
else "Violation - Invalid Match Criteria"
else result
let result = if numSequences > 1
then "Violation - Duplicate Route-Map Sequence"
else result
select distinct {
violation: result not in i"Passed", "Not Checked"],
Result: result,
device: device.name,
"Route Map": object.name,
"In Use": object.name in routeMapUsages,
seq: object.seq,
type: object.objectType,
objectName: object.objectName
}

 

Be the first to reply!

Reply