相关文章推荐
首发于 中文编程
用python编写控制网络设备的自动化脚本5:访问控制列表

用python编写控制网络设备的自动化脚本5:访问控制列表

前言

访问控制列表(Access Control List)在网络设备中是一种很常见的数据结构,它的功能是匹配地址,匹配成功则根据动作确定是允许(permit)还是拒绝(deny)。访问控制列表可以应用在很多地方,在不同场合有不同的功能,最常见的就是应用在(网络)接口中,用来过滤数据包。

本文章写了脚本对于访问控制列表的内部实现过程,利用这个脚本可以解析目标设备配置中的访问控制列表,根据访问控制列表规则做一些自己想要的操作。

注:本文章中单独出现的 序号 访问控制列表号(Access Control List Number) 规则序号 指访问控制列表 规则序号(Rule Sequence/Rule Number)

统一的访问控制列表规则结构

观察网络设备中访问控制列表的结构,可以知道一条访问控制列表规则有这些字段:

  • 规则序号
  • 动作
  • 协议
  • 源地址
  • 源端口号
  • 目的地址
  • 目的端口号
  • 其他选项

虽然访问控制列表有很多类型,其中的字段和类型也各不相同。为了方便起见,我直接用一个结构来表示所有的不同类型的访问控制列表规则。反正Python是动态类型语言,不管用不用得到先占个位置。

class S访问控制列表规则:
    def __init__(self, **a):
        self.m序号 = -1
        self.m允许 = None
        self.m协议 = E协议.ip
        self.m源地址 = None
        self.m目的地址 = None
        self.m源端口 = None
        self.m目的端口 = None
        self.f更新(**a)
    def f更新(self, a规则 = None, **a字典):
        pass #省略

统一的端口号结构

访问控制列表中,端口号不只有数字,还带有一个比较符号,用来给定一个端口号范围。为了让脚本能支持乱七八糟的端口号格式,我定义了一个端口号结构:

这个结构很简单,只有2个值:数字、运算符。

class S端口号:
    def __init__(self, a值, a运算):
        self.m运算 = a运算
        self.m值 = a值
    @staticmethod
    def fc大于(a值):
        return S端口号(a值, 运算.f大于)
    @staticmethod
    def fc大于等于(a值):
        return S端口号(a值, 运算.f大于等于)
    @staticmethod
    def fc小于(a值):
        return S端口号(a值, 运算.f小于)
    @staticmethod
    def fc小于等于(a值):
        return S端口号(a值, 运算.f小于等于)
    @staticmethod
    def fc范围(a值):
        return S端口号(a值, 运算.f包含)
    @staticmethod
    def fc等于(*a值):  #慎用多值,只有思科ipv4扩展访问控制列表支持多值
        return S端口号(a值, 运算.f包含)
    @staticmethod
    def fc不等于(*a值): #慎用,只有思科支持不等于
        return S端口号(a值, 运算.f不包含)
    def fi范围内(self, a):
        return self.m运算(self.m值, a)

创建端口号值的方法就是调用这些“fc”开头的函数

v端口号1 = S端口号.fc等于(100)  #=100
v端口号2 = S端口号.fc大于(200)  #>200
v端口号3 = S端口号.fc范围(range(2000, 3000))    #[2000,3000)

端口号表达式

虽然端口号结构很方便,但是实际使用中写起来太长了。为了方便写端口号,我发明了一个端口号表达式,直接把一个类似于“运算 值”格式的字符串转换成端口号值

#↓S端口号.fc字符串
    @staticmethod
    def fc字符串(a: str):
        if a.isdigit():
            return S端口号.fc等于(int(a))
        if 字符串.fi连续范围(a) or 字符串.fi区间范围(a):
            return S端口号.fc范围(字符串.ft范围(a))
        #"符号 数字"的格式
        ca符号表 = {
            "==": S端口号.fc等于,
            ">=": S端口号.fc大于等于,
            "<=": S端口号.fc小于等于,
            "!=": S端口号.fc不等于,
            "<>": S端口号.fc不等于,
            ">": S端口号.fc大于,
            "≥": S端口号.fc大于等于,
            "<": S端口号.fc小于,
            "≤": S端口号.fc小于等于,
            "=": S端口号.fc等于,
            "≠": S端口号.fc不等于,
        for k, v in ca符号表.items():
            v符号长度 = len(k)
            v符号 = a[0 : v符号长度]
            if k == v符号:
                v数字 = int(a[v符号长度 : ])
                return v(v数字)
        raise ValueError("无法解析的格式")

端口号表达式可以用下面几种格式:

  • 数字:“80”
  • 比较符号 数字:“>100”
  • 区间:“[100, 200)”。跟数学区间一样,圆括号是开,方括号是闭。
  • 连续:“100~200”。这个包含左右值。

访问控制列表配置模式(编程)接口

访问控制列表配置模式用来修改一个访问控制列表,可以添加、删除、修改规则,功能上可以说很少。

class I访问控制列表(I模式):
    def __init__(self, a):
        I模式.__init__(self, a)
    def fs规则(self, a序号 = None, a规则 = None, a操作 = E操作.e设置):
        raise NotImplementedError()
    def fe规则(self):
        raise NotImplementedError()
    def f应用到(self, a模式, a方向 = E方向.e入, a操作 = E操作.e设置):
        raise NotImplementedError()

fs规则 用来修改配置,可以新建、添加、删除、修改规则,把操作都放在一个函数中。

fe规则 则遍历该列表的所有规则,它会解析配置然后转换成 S访问控制列表规则 值,用于外部判断。

f应用到 用来把当前列表应用到某个模式中,比如(网络)接口配置模式,登录配置模式,路由策略。当然目标模式也有相对应的应用访问控制列表函数,不管是从哪边调用,效果都是一样的。

在 I全局配置模式 中,有一个用来进入访问控制列表配置模式的 f模式_访问控制列表。只要提供名称和类型,它就会创建相应的模式对象:

def f模式_访问控制列表(self, a名称, a类型 = None, a操作 = E操作.e设置):
        raise NotImplementedError()

这里的 a名称 是一个访问控制列表名,可以是一个字符串也可以是一个数字,但是用数字容易采坑。而 a类型 要传入一个访问控制列表类型枚举,这个枚举定义了不同的访问控制列表类型:

class E访问控制列表类型(enum.IntEnum):
    e接口 = 0x1000
    e物理 = 0x2000
    e标准4 = 0x3040
    e扩展4 = 0x3041
    e标准6 = 0x3060
    e扩展6 = 0x3061

要创建模式对象,这个类型是必须的,因为必须指定一个类型才能创建对应的对象。但是这里的类型又可以省略,因为某些情况下可以通过名称推断出对应类型,这个下面会介绍。

访问控制列表配置模式类

访问控制列表有很多种类型,写完(编程)接口之后,还要根据具体类型定义具体的类。由于同一品牌网络设备的访问控制列表配置命令是大同小异的,所以可以继续提取出(编程)接口。

下面以思科为例,写一个思科的访问控制列表(编程)接口和具体的访问控制列表类。

class I思科访问控制列表(I访问控制列表):
    def __init__(self, a, a名称, a类型: str = "", a协议: str = "ip"):
        I访问控制列表.__init__(self, a)
        self.m名称 = a名称
        self.m类型 = a类型
        self.m协议 = a协议
    def fg模式参数(self):
        return (self.m类型, self.m名称)
    def fg进入命令(self):
        return "%s access-list %s %s" % (self.m协议, self.m类型, self.m名称)
    def fg显示命令(self, a序号 = None):
        v命令 = 设备.C命令("show %s access-list %s" % (self.m协议, self.m名称))
        if a序号 != None:
            v命令 += "| include ^____%d_" % (a序号,)
        return v命令
    def f添加规则(self, a序号, a规则):
        raise NotImplementedError()
    def f删除规则(self, a序号: int):
        self.f执行当前模式命令(c不 + str(a序号))
    def fs规则(self, a序号 = 通用访问列表.c空序号, a规则 = 通用访问列表.c空规则, a操作 = 设备.E操作.e添加):
        v操作 = 通用实用.f解析操作(a操作)
        if v操作 == 设备.E操作.e设置:
            self.f添加规则(a序号, a规则)
        elif v操作 == 设备.E操作.e新建:
            self.f添加规则(a序号, a规则)
        elif v操作 == 设备.E操作.e修改:
            v序号 = a序号 if a序号 >= 0 else a规则.m序号
            v规则 = self.fg规则(v序号)
            v规则.f更新_规则(a规则)
            self.f删除规则(v序号)
            self.f添加规则(v序号, v规则)
        elif v操作 == 设备.E操作.e删除:
            self.f删除规则(a序号)
    def fe规则0(self, af解析):
        v命令 = self.fg显示命令()
        v输出 = self.m设备.f执行显示命令(v命令)
        v位置 = 字符串.f连续找最后(v输出, "access list", "\n")
        for v行 in v输出[v位置+1:].split("\n"):
            if v行[0:4] != " ":
                continue
            yield af解析(v行)
    def fe规则(self):
        return self.fe规则0(self.f解析规则)
    @staticmethod
    def f解析规则(self):
        raise NotImplementedError()
# ↓具体实现
class C六(I思科访问控制列表):
    "互联网协议第6版命名访问控制列表"
    def __init__(self, a, a名称):
        I访问控制列表.__init__(self, a, a名称, a协议 = "ipv6")
    def f添加规则(self, a序号, a规则):
        v命令 = 设备.C命令()
        v命令 += f生成规则序号6(a序号)
        v命令 += f生成允许(a规则.m允许)
        v命令 += 通用访问列表.ca协议到字符串6[a规则.m协议]
        v层 = 通用实用.f取协议层(a规则.m协议)
        if v层 == 3:
            v命令 += f生成地址6(a规则.m源地址)
            v命令 += f生成地址6(a规则.m目的地址)
        elif v层 == 4:
            v命令 += f生成地址6(a规则.m源地址)
            v命令 += f生成端口(a规则.m源端口)
            v命令 += f生成地址6(a规则.m目的地址)
            v命令 += f生成端口(a规则.m目的端口)
        else:
            raise NotImplementedError("迷之逻辑")
        #执行命令
        self.f执行当前模式命令(v命令)
    @staticmethod
    def f解析规则(a规则: str):
        v解析器 = C规则解析器(a规则)
        v规则 = S访问控制列表规则()
        v规则.m允许 = v解析器.f允许()
        v规则.m协议 = v解析器.f协议()
        v规则.m源地址 = v解析器.f地址6()
        v规则.m源端口 = v解析器.f端口号()
        v规则.m目的地址 = v解析器.f地址6()
        v规则.m目的端口 = v解析器.f端口号()
        v规则.m序号 = v解析器.f序号6()
        return v规则

首先,思科的访问控制列表配置模式(编程)接口的 fs规则 会判断参数里的操作,确定是添加规则还是删除规则,然后调用相应函数。思科删除规则命令是一样的,所以直接把实现写在接口里。而不同类型的访问控制列表的添加规则的命令是不一样的,所以把添加规则放到各个具体实现中。

思科的访问控制列表配置模式(编程)接口同时也实现了 fe规则 函数,它直接把临时函数 fe规则0 和具体实现的 f解析规则 包装起来,返回一个生成器。具体类中只需要写如何把规则字符串转换成 S访问控制列表规则 值即可。

这里有一堆 f生成xxx 函数,还有一个规则解析器,都是为了简化代码、提高代码重用率而写的,反正就是一堆规则值和字符串互相转换的函数,没什么好说的,这里省略不写。

统一序号到设备特定序号的转换

不同品牌不同型号不同类型的访问控制列表序号各不相同。比如思科的标准访问控制列表序号范围是1~99和1300~1399,华为华三的标准访问控制列表序号范围是2000~2999。这种分裂的序号范围不利于跨型号大批量自动配置,需要统一。

为了实现这种统一行为,我写了一个访问控制列表助手(编程)接口,这个接口定义了从统一序号到设备特定序号互相转换的函数。统一序号对于不同的访问控制列表类型可以重叠,且按照编程习惯从0开始,不设上限,需要的时候再转换成特定序号。

class I访问控制列表助手:
    "用来计算到目标设备的访问控制列表序号, 原始参数的n从0开始"
    @staticmethod
    def ft特定序号(n, a类型):
        return n
    @staticmethod
    def ft统一序号(n, a类型 = None):
        return n
    @staticmethod
    def f判断类型(n):
        "根据特定序号判断类型"
        raise NotImplementedError()

这个接口的前2个函数用作统一序号和特定序号的互相转换,第3个函数下面会介绍。在 f模式_访问控制列表 中,a名称 可以传数字,这时候会被当做特定序号。如果要当成统一需要就要在数字外面包点东西。

对于思科设备而言,标准列表统一序号0转换成标准列表特定序号是1,标准列表统一序号99到标准列表特定序号是1300。统一序号的连续性可以让用户无需关心具体设备的序号范围。

思科设备的访问控制列表序号范围终究是有上限的,而统一序号是无上限的。为了保证统一序号超出设备可用序号还能兼容,需要把访问控制列表序号转换成访问控制列表名称。

有了上面万无一失的转换规则,从统一序号到特定序号的映射如图所示:

统一序号可以转换成特定序号,特定序号也可以转换成统一序号。但是从特定序号到统一序号转换的过程中存在不按套路的情况,用户可能会尝试把特定序号100转换成标准访问控制列表统一序号。由于100在思科中是扩展访问控制列表序号,不是标准访问控制列表序号,只有错误调用才会出现这种情况,在转换过程中发现序号不对直接抛异常即可。

另外,在 f模式_访问控制列表 中,a名称 传数字时会被当做特定序号。如果要当成统一序号,需要在数字外面包点东西,所以我又写了一个结构。

class S统一序号:
    def __init__(self, a统一, a特定 = None, a类型 = None):
        self.m统一序号 = a统一
        self.m特定序号 = a特定
        self.m类型 = a类型
    def __str__(self):
        return str(self.m特定序号)
    @staticmethod
    def fc特定序号(n, a类型 = None):
        return S统一序号(None, n, a类型)
    @staticmethod
    def fc统一序号(n, a类型 = None):
        return S统一序号(n, None, a类型)

然后在调用 f模式_访问控制列表 时传入统一序号:

v访问列表 = v全局配置.f模式_访问控制列表(S统一序号(0), E访问控制列表类型.e标准4)

这样就能清晰表达出意图了。

根据特定序号判断访问控制列表类型

当访问控制列表名称为数字时,这个数字所在范围代表了这个访问控制列表是什么类型。在很多设备中,经常可以看到进入访问控制列表配置模式时直接省略类型,比如华为设备可以这么写:

acl 2000    #进入基本访问控制列表2000视图
acl 3000    #进入高级访问控制列表3000视图

在脚本中,不同的访问控制列表类型对应着不同的类。既然具体设备允许省略类型,那么脚本中也应该允许省略类型,方便偷懒。

为了让脚本能够实现根据序号判断类型的功能,上面的 I访问控制列表助手.f判断类型 就是用来干这活,它根据传入的特定序号,返回相应的访问控制列表类型。

#在华为/华三中长这样
    @staticmethod
    def f判断类型(n):
        try:
            v = int(n)
            if v in range(2000, 3000):
                return E访问控制列表类型.e标准4
            elif v in range(3000, 4000):
                return E访问控制列表类型.e扩展4
            else:
                return None
        except:
            return None

实际使用中,在已知什么序号对应什么类型的情况下就可以直接写数字。

v访问列表配置 = v全局配置.f模式_访问控制列表(2000)    #根据序号推断类型

这行代码在不同设备有不同的结果。如果这是一个思科设备,脚本会判断为“扩展(Extended)访问控制列表”;如果这是一个华为/华三设备,脚本会判断为“基本(Basic)访问控制列表”。

这个功能主要用在从配置中获取访问控制列表,又要修改访问控制列表的情况。有时候某个地方引用了一个访问控制列表,一般只能获取到一个名称,不包含类型信息,要配置访问控制列表的话必须指定一个类型,这时候让脚本来推断类型的话可以少打很多代码。

直接在代码中匹配地址和端口号

在脚本中,可能要判断某个地址某个端口号是否匹配某条规则,根据匹配结果做不同事情。不同品牌网络设备的访问控制列表的功能都是一样的,所以可以把这个功能写进脚本中。

在 S访问控制列表规则 中添加一个函数 f匹配,功能就是匹配地址和端口号:

    def f匹配(self, a源地址 = None, a源端口 = None, a目的地址 = None, a目的端口 = None):
        def f匹配0(a成员, a匹配):
            if a成员 != None and a匹配 != None:
                return a成员.fi范围内(a匹配)
            else:
                return True
        v匹配源地址 = f匹配0(self.m源地址, a源地址)
        v匹配目的地址 = f匹配0(self.m目的地址, a目的地址)
        v匹配源端口 = f匹配0(self.m源端口, a源端口)
        v匹配目的端口 = f匹配0(self.m目的端口, a目的端口)
        v匹配 = v匹配源地址 and v匹配目的地址 and v匹配源端口 and v匹配目的端口
        if v匹配:
            return self.m允许
        else:
            return None

这里的判断条件写得很宽松,None表示任意匹配,先逐个值一一匹配。全部匹配完后再判断是否匹配整条规则,最后根据动作确定是否允许,返回True和False,如果不匹配规则,返回None。

应用示例:智能增删改访问控制列表

这个示例开3个模拟器,每个模拟器各摆2台设备,总共6台设备做实验。

6台设备的参数如下所示:

  • 设备1,名称:“R1”,型号:思科C7200
  • 设备2,名称:“SW2”,型号:思科L2 IOU
  • 设备3,名称:“R3”,型号:华为NE40E
  • 设备4,名称:“SW4”,型号:华为S5700
  • 设备5,名称:“R5,型号:华三MSR36-20
  • 设备6,名称:“SW6”,型号:华三S5820V2

3个模拟器6台设备全部开起来,电脑内存爆炸(还行)

6台设备只配置,不连线。

6台设备的配置分别为(随便敲的):
R1

hostname R1
ip access-list standard 1
 10 deny host 10.0.0.1
 20 permit host 10.0.0.2
 30 permit host 10.0.0.3
line vty 0 4
 access-class 1 in

SW2

hostname SW2
ip access-list standard 1
 10 permit host 10.0.0.1
ip access-list standard 2
 10 deny 10.0.0.0 0.0.0.255
 20 permit 10.1.0.0 0.0.0.255
line vty 0 4
 access-class 2 in

R3

sysname R3
acl 2003
 rule 5 deny source 10.0.0.1 0
 rule 10 permit source 10.0.0.0 0.0.0.255
user-interface vty 0 4
 acl 2003 inbound

SW4

sysname SW4
acl 2004
 rule 5 permit source 20.0.0.0 0.255.255.255
 rule 10 deny source 10.0.0.1 0
 rule 15 deny source 10.0.0.0 0.0.0.255
user-interface vty 0 4
 acl 2004 inbound

R5

sysname R5
telnet server acl 2005
acl basic 2005
 rule 5 permit source 10.0.0.2 0
 rule 10 deny source 10.0.0.0 0.0.0.255

SW6

sysname SW6
ssh server acl 2006
acl basic 2006
 rule 5 permit source 10.0.0.2 0
 rule 10 permit source 10.0.0.3 0

实验内容
查找登录配置中绑定的访问控制列表,根据列表做相应配置:

  • 如果列表中没有允许10.0.0.1这个地址,则添加一条允许10.0.0.1/32的规则。

有一点需要注意,一般在访问控制列表中,“没有允许”意味着“拒绝”或“规则不存在”。如果原规则是拒绝一段地址,贸然改掉会产生副作用。为了做到只允许10.0.0.1这个地址,只需在这条拒绝规则前添加一条允许规则即可。

写代码:

import cflw网络连接 as 连接
import cflw网络连接_串口 as 串口
import cflw网络设备 as 设备
import cflw网络设备_思科 as 思科
import cflw网络设备_华为 as 华为
import cflw网络设备_华三 as 华三
ca设备信息 = [
    #创建连接函数     连接参数                        创建设备函数  型号              版本
    (连接.C网络终端,  ("gns3.localhost", 5000),       思科.f创建设备,   思科.E型号.c7200,   15.2),
    (连接.C网络终端,  ("gns3.localhost", 5001),       思科.f创建设备,   思科.E型号.l2iou,   15.1),
    (连接.C网络终端,  ("ensp.localhost", 2000),       华为.f创建设备,   华为.E型号.ne40e,   8.18),
    (连接.C网络终端,  ("ensp.localhost", 2001),       华为.f创建设备,   华为.E型号.s5700,   5.20),
    (串口.C命名管道,  (r"\\.\pipe\topo1-device1",),   华三.f创建设备,   华三.E型号.msr3620, 7.1),
    (串口.C命名管道,  (r"\\.\pipe\topo1-device2",),   华三.f创建设备,   华三.E型号.s5820v2, 7.1),
def f创建设备(a设备信息):
    v连接 = a设备信息[0](*a设备信息[1])
    v设备 = a设备信息[2](v连接, a设备信息[3], a设备信息[4])
    return v设备
def main():
    for v设备信息 in ca设备信息:
        v设备 = f创建设备(v设备信息)
        print("\n" + "=" * 40)
        v用户模式 = v设备.f模式_用户()
        v用户模式.f登录()
        v设备.fs回显(True)
        v用户模式.fs终端监视(False)
        v全局配置 = v用户模式.f模式_全局配置()
        v登录配置 = v全局配置.f模式_登录(设备.E登录方式.e虚拟终端, range(0, 5))
        v访问列表名 = v登录配置.fg访问控制列表()
        v访问列表配置 = v全局配置.f模式_访问控制列表(v访问列表名)
        v条件1 = False    #允许10.0.0.1
        c新规则 = 设备.S访问控制列表规则(a允许 = True, a源地址 = "10.0.0.1")
        for v规则 in v访问列表配置.fe规则():
            v匹配1 = v规则.f匹配(a源地址 = "10.0.0.1")
            if v匹配1 == True and v条件1 == False:
                v条件1 = True
                break
            elif v匹配1 == False and v条件1 == False:
                if v规则.m源地址.fi主机():
                    v访问列表配置.fs规则(v规则.m序号, c新规则, a操作 = 设备.E操作.e修改)
                else:
                    v访问列表配置.fs规则(v规则.m序号-1, c新规则, a操作 = 设备.E操作.e新建)
 
推荐文章