防火墙自动化(一) 防火墙的配置解析

1,919 阅读17分钟

网络运维工程师日常打交道的设备,无非是路由器交换机还有防火墙。防火墙在其中,属于有一定技术含量,但又比较繁琐的部分。为了更细粒度的企业安全,企业环境中的防火墙一般都是先deny all,再逐个开启需要访问的网络关系,而网络关系的载体就是一条条的防火墙策略,这样就带来了大量的防火墙策略配置工作。在过往的防火墙管理中,经常需要付出一定的人力物力去执行防火墙的策略生成和下发工作,如何对防火墙进行自动化,削减这方面的工作。本系列会分专题进行阐述并针对实际产生的问题提出优化建议。

  首先是防火墙的配置解析。本文主要针对Juniper防火墙,H3C防火墙,思科ASA防火墙,给出配置解析的一般思路,并提供参考代码。分为以下三点进行阐述。

  • 需要明确的前置问题
  • 各种解析相关的知识
  • 防火墙配置解析代码

1. 需要明确的问题

在进入正式的防火墙配置解析之前,先对以下这些问题做一些说明。

1.1 如何获取防火墙现有的信息?有哪些途径?

[HTTP/HTTPS]

部分厂商部分型号的防火墙是支持web页面管理的,也就是说支持通过80/8080/443端口进行信息的查询和修改。这类防火墙的官网一般都会有对应的 API文档,如果所在企业已经购买了产品和服务,可以向厂商或代理商索要对应的API文档。但也存在部分厂商或产品有web界面但给不出对应的API文档的情况,此时处理过程可能要麻烦一些,需要通过分析web页面的方式获取到背后的数据API。利用API文档中的API或分析出来的API,我们可能可以获取到的信息包括防火墙策略、nat策略、运行状态等。

image.png (web界面示例)

[SSH]

一般的防火墙都带有远程ssh管理功能(如无,请尽早退货),如果可以通过secureCRT、putty、Teminal这些工具访问到防火墙的管理页面,则可以通过SSH协议获取到我们想要的策略数据。通用的做法是用代码模拟SSH登录,在输入流里输入对应的命令,从输出流里获取到我们需要的数据。

image.png (SSH界面示例)

[SNMP]

作为专门设计用于在 IP 网络管理网络节点(服务器、工作站、路由器、交换机及HUBS等)的一种标准协议,基本上防火墙设备都支持这种协议。如果防火墙开通了SNMP功能,可以视作一个key-value形式的数据库,我们只要拿着OID这个key,通过SNMP方法就能取出对应的value.这种方法适合用于监控,实时状态信息一般都能从这个数据库取到。 image.png (SNMP抓取数据示例)

[NETCONF协议]

这类协议也是可以获取到防火墙的网络设备的信息,但是依赖于专门的厂商,并且使用到xml格式进行传输。具体能获取到的信息和HTTP/HTTPS类似,也可以对网络设备进行管理。

image.png (NETCONF使用的XML示例)  

1.2 上述获取信息的途径有哪些区别?

       首先理解一点,现在商用的防火墙或其他网络设备,大多都是转发和控制层面使用的CPU会进行分离(ps:转发和控制分离是SDN)。所以即使是控制层面的CPU性能消耗殆尽,也会停留在控制层面瘫痪,下面转发还是会按原有的规则转发,但这样就已经无法对防火墙做任何修改,已经属于比较严重的事故。上面四种途径,都需要在控制层面(可以看作一个linux系统)启动后台进程,提供出来的服务,都需要消耗控制层面的CPU和内存,所以必须要尽量高效地使用。

[HTTP/HTTPS]

如果拿到了API文档,开发过程会十分简单,API是规律的,返回数据也一般是json格式,在python中可以直接使用。但开启HTTP/HTTPS服务消耗防火墙CPU和内存会较多。

[SSH]

开发起来也比较简单,数据以文本流输入输出,输出时可能会有一定的数据粘连问题,以及获取到文本后,还需要二次解析才能建立对应的映射。SSH消耗防火墙CPU和内存会较少。

[SNMP]

开发起来比较复杂,因为有相当多的OID,需要根据情况挑选OID,再去取数据。key-value类型的取数据方式导致的是取单个数据最快,取连续大块数据最慢。比如同样的一个CPU使用率值,如果使用SNMP去取,时间开销肯定是最少的,但如果我想去取一整个路由表下来,则需要花费最长的时间。所以SNMP适用于监控场景,较少用于大块数据的获取。

[NETCONF协议]

开发过程和消耗都和HTTP/HTTPS方式差不多,可能略低于HTTP/HTTPS,但开发中使用XML格式进行传输导致有额外的学习和使用成本。

1.3上述途径获取到的防火墙信息是什么形式?需不需要解析?

[HTTP/HTTPS]

获取到的信息一般都为JSON格式,一般可以从HTTPresponse中取出来直接使用,有可能需要编写程序解析。

[SSH]

获取到的信息一般都为文本格式,返回值一般要经过编写程序解析,才能投入后续自动化开发的使用。

[SNMP]

获取到的信息一般都为文本格式的单个值,不需要太多额外的解析。

[NETCONF协议]

获取到信息一般都为XML格式,需要解析,可以使用特定的包进行解析。

 

1.4 获取防火墙策略的最佳方式是什么?为什么?

       这个问题实际上要结合实际情况来回答。我只给出我倾向于使用什么,以及使用的理由。

       个人倾向是HTTP/HTTPS>SSH>NETCONF>SNMP。

       SNMP排末尾是因为不知道能不能取出来,到底有没有这个OID,因为通用OID范围内是不包含这个信息的。

       NETCONF排倒二是因为xml的解析比较麻烦,而且是专有协议,开发出来只能CISCO设备使用。

       SSH作为次选,是可以一次性取出所有配置的,相当于一次性取出所有策略。但是通过配置解析出防火墙策略需要对防火墙策略很熟悉。

       HTTP/HTTPS体验最佳,一次或多次请求,就能获取到json格式的策略数据。但是防火墙不一定有HTTP/HTTPS功能,有这个功能不一定会允许开(别问我为什么,一般是领导觉得有风险不给开),所以能不能用上还得看情况。

       所以下面的内容都基于SSH取到完整的配置,然后对之进行解析。以下为SSH取数据的简单示例,关于SSH如何取数据,已经有大量可搜索到文章讲解,此处不在赘述。

#encoding=utf-8
import paramiko
import socket
import time
import re

class SSHConnection(object):
    
    def __init__(self, host, port, username, password):
        self._host = host
        self._port = port
        self._username = username
        self._password = password

    def get_connect(self):
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(self._host, 22, self._username, self._password, timeout=300, allow_agent=False,
                    look_for_keys=False)
        return ssh

    def close(cls, ssh):
        if ssh:
            ssh.close()

class SSHInfo(object):

    def __init__(self,ip,username, password,port=22,delay=0.5,buffsize =99999999):
        self.ip = ip
        self.usename = username
        self.password = password
        self.port = port
        self.delay = delay
        self.buffsize = buffsize
        self.conn = SSHConnection(ip, 22, username, password)
        self.ssh = self.conn.get_connect()
        self.ch = self.ssh.invoke_shell()

    def read_buff(self, nbyte, delay=0.5, greedy_mode=True):
        buff = ""
        try:
            self.ch.settimeout(3)
            time.sleep(delay)
            if greedy_mode:
                while True:
                    _buff = self.ch.recv(nbyte)
                    try:
                        buff += str(_buff, encoding = "utf-8")
                    except:
                        buff += str(_buff, encoding="gbk")
            else:
                buff = self.ch.recv(nbyte)
        except socket.timeout as ex:
            pass
            print("{}:{} read buff to end: {}(socket timeout)".format(self.ip,self.port,ex))
        except Exception as ex:
            more = traceback.format_exc()
            print(more)
            print("{}:{} {}".format(self.ip,self.port,ex))
        return buff

    def runCmd(self, cmd):
        _cmd = cmd + "\n"
        print("{}:{} {}".format(self.ip,self.port,cmd))
        time.sleep(self.delay)
        self.ch.send(_cmd)
    
    def getInfo(self, cmd):
        self.runCmd(cmd)
        time.sleep(self.delay)
        config_content = self.read_buff(self.buffsize, 5)
        idx = config_content.rfind('\r\n')
        self.runCmd("quit")
        return config_content[:idx]
    
    def getInfoNoQuit(self, cmd):
        self.runCmd(cmd)
        time.sleep(self.delay)
        config_content = self.read_buff(self.buffsize, 5)
        idx = config_content.rfind('\r\n')
        return config_content[:idx]

cn = SSHInfo(ip, username, password, port, delay, buffsize) #请自定对括号中的变量进行定义
cn.runCmd("show config")
content = cn.read_buff(100000)
print(content)

2. 各种解析相关的知识

2.1 点分十进制如何转化成int数

每个独立的IP地址都能用一个单独的int数来存储,点分十进制字符串形式和int形式的IP地址在一定意义上是等价的。

下图从完整的二进制数据包开始,到三层IP头部的构成,再到IP头部中源地址的构成,最终描述了点分十进制字符串形式和int形式的IP的关系。

image.png

由上图可知,不管是使用int形式,还是点分十进制字符串形式,它们都可以对一个二进制的IP地址进行准确描述,并且两者之间存在一定的转化关系。高效的转化关系用python函数可以描述为:

#encoding=utf-8
import struct
import socket

def ip2num(ip):
    """将点分十进制ip转化成int数"""
    return struct.unpack("!L", socket.inet_aton(ip))[0]

def num2ip(num):
    """将int数转化成对应的点分十进制ip"""
    a, c, b, d = (num & 0xff000000) >> 24, (num & 0x0000ff00) >> 8, (num & 0x00ff0000) >> 16, (num & 0x000000ff) >> 0
    return "{}.{}.{}.{}".format(a, b, c, d)

2.2 何时用int数存储IP

点分十进制字符串形式和int形式,在表示IP地址时各有优缺。点分十进制字符串易读,日常使用的多半是是直接以这种格式存储。而什么时候使用int数存储IP?这里列出了常用的三种情况:

2.2.1 节省存储空间

如果使用字符串来存储IP地址,使用ASCII编码,每个IP地址需要7到15个字节;使用utf-8或者gbk编码,则会更多。存储的IP地址很多时,这将是一笔庞大的存储开销。而如果将IP地址转化成INT(uint32)数存储,每个IP地址固定消耗一个字节的的空间,缩减存储空间可达50%以上。

ip1 = "1.1.1.1"           # 7个字符,如使用ASCII编码,占用7个字节
ip2 = "111.111.111.111"   # 15个字符,如使用ASCII编码,占用15个字节
ip1 = 16843009            # "1.1.1.1"的INT形式,32位INT数,占用4个字节
ip2 = 1869573999          # "111.111.111.111"的INT形式,32位INT数,占用4个字节

2.2.2 方便顺序查询

如果有查询整个网段IP地址的需求,比如说要查询1.1.4.0/24这个网段。IP字符串存储,返回结果的顺序会错位,如"1.1.4.1"后面的IP是"1.1.4.10",而非"1.1.4.2",需要重新排序。但如果是使用INT数的形式,范围查询和顺序查询都会变得比较简单,直接使用">" 、"<"这样的操作即可,匹配表项的速度会很快;同时,因为IP已经转化成数字了,分库分表也会很简单,直接用数字映射到对应的表即可,比如0-100000使用iptable表1,100000-200000使用iptable表2,可以有效将数据分开存储,支持更多的并发查询。

//ip使用字符串存储,查询1.1.4.0/24网段
mysql>SELECT ip,state FROM ipble where ip like "1.1.4.%";
//返回结果为字符串,按字符串排序方式进行排序
| 1.1.4.1   | 0 |
| 1.1.4.10  | 0 |
| 1.1.4.100 | 1 |
| 1.1.4.101 | 1 |
| 1.1.4.102 | 1 |
| 1.1.4.103 | 1 |


//ip使用INT数存储,查询1.1.4.0/24网段
mysql>SELECT ip,state FROM ipble where ip >=16843776 and ip <=16843776+256;
//返回结果为int数,不易人工读,按数字排序方式进行排序
| 16843777 | 0 |
| 16843778 | 0 |
| 16843779 | 1 |
| 16843780 | 1 |
| 16843781 | 1 |
| 16843782 | 1 |

2.2.3 IP网段快速匹配

IP网段可以理解为IP的复数形式,多个IP地址可以组成一个IP网段。为了存储IP网段,最通用的做法是,在IP的基础上引入了一个新的量也就是MASK(掩码),用SUBNET表示IP网段,SUBNET = (IP address, MASK)。MASK和IP地址一样,也可以进行 1.1 中的点分十进制形式和int形式互转。如果需要判断一个IP地址是否在一个网段中,可以直接通过如下的位运算进行判断,效率会非常高(更高效率可以用C代码)。但因必须是int形式才可以进行下面位运算,所以这种情况下也必须用int形式的IP地址。

#encoding=utf-8
ipaddress = 3232235777                #"192.168.1.1"的int形式
subnet = (3232235776,4294967040)      #("192.168.1.0","255.255.255.0")的int形式

def isIpInSubnet(ipaddress,subnet):
    """快速判断一个int形式的ip是否在subnet中"""
    base, mask = subnet
    if ip&mask==base&mask:
        return True
    return False
    
print(isIpInSubnet(ipaddress,subnet))

2.3 IP网段的三种常用表现形式

在生产环境中,我们一般会有三种IP网段的表现形式,这些形式被广泛用于防火墙规则编写中。

  • 初始IP地址+子网掩码 (base,subnet mask)
  • 初始IP地址+通配符掩码 (base,wildcard mask)
  • 初始IP地址+末尾IP地址 (base,end) 这三种IP网段的表现形式在生产环境均有应用,但根据公司或组织的不同,约定的内部规范不同,采用的表现形式也不同。下文会对这三种表现形式逐一讲解。

在了解这三种表现形式之前,先了解一下子网掩码和通配符掩码的区别。

"""
子网掩码:
    点分十进制形式:255.255.255.0
    二进制形式:1111111111111111111111100000000
    规律:二进制形式前面全部为1,后面全部为0
    
通配符掩码:
    点分十进制形式:127.255.31.0
    二进制形式:1111111111111110001111100000000
    规律:1和0可以交替使用,不需要前面全部为1
tips:两种掩码的IP网段,都可以用1.2.3中的isIpInSubnet判断某个IP地址在不在这个网段中。
"""

接下来是这三种形式的说明,假设下面的base,subnet mask,wildcard mask,end都已经通过1.1中ip2num函数转化成了int数。

2.3.1 初始IP地址+子网掩码 (base,subnet mask)

在思科ASA防火墙配置中,会有这样的定义语句

object network ALL_net
 subnet 70.0.0.0 255.0.0.0
object network TB-SW
 subnet 70.2.12.0 255.255.255.0
object network TS-SW
 subnet 70.2.24.0 255.255.255.0

使用的便是初始IP地址+子网掩码的方式定义IP网段,这些IP网段存在以下转化:

#encoding=utf-8

#base_str为IP网段的开始,一般为形如"192.168.1.0"的字符串
base_str = "192.168.1.0"
#submask_str为IP网段的子网掩码,一般为形如"255.255.255.0"的字符串
submask_str = "255.255.255.0"

#base_str,submask_str的int形式,用到了1.1的ip2num
base = ip2num(base_str)       #3232235776
submask = ip2num(submask_str) #4294967040

#IP网段包含的IP地址个数,以及反掩码的int形式
ipnum = (1<<32)-1-submask    #256

#子网掩码的反掩码的字符串形式
revmask = num2ip(ipnum)      #"0.0.0.255"

#判断某个ip是否在该IP网段中,可以使用1.2.3中的isIpInSubnet方法
isIpInSubnet("192.168.1.1",(base,widcmask)) #True

2.3.2 初始IP地址+通配符掩码 (base,wildcard mask)

在H3C的防火墙配置中,会有这样的定义语句

object-group ip address dbserver
 0 network subnet 40.10.21.0 255.255.255.0
 10 network subnet 45.20.21.0 255.255.255.0
 20 network subnet 45.0.16.0 wildcard 0.255.0.255

其中的“20 network subnet 45.0.16.0 wildcard 0.255.0.255”使用的便是通配符掩码定义IP网段,这些IP网段存在以下转化:

#encoding=utf-8

#base_str为IP网段的开始,一般为形如"192.168.1.0"的字符串
base_str = "192.168.1.0"
#submask_str为IP网段的通配符掩码,一般为形如"127.255.31.0"的字符串
widcmask_str = "127.255.31.0"

#base_str,submask_str的int形式,用到了1.1的ip2num
base = ip2num(base_str)        #3232235776
widcmask = ip2num(widcmask_str) #2147426048

#IP网段包含的IP地址个数,以及反掩码的int形式
ipnum = (1<<32)-1-submask    #2147541247

#子网掩码的反掩码的字符串形式
revmask = num2ip(ipnum)      #"128.0.224.255"

#判断某个ip是否在该IP网段中,可以使用1.2.3中的isIpInSubnet方法
isIpInSubnet(ip2num("192.168.1.0"),(base,widcmask)) #True

2.3.3 初始IP地址+末尾IP地址 (base,end)

在思科ASA防火墙中,存在这样的定义语句

object network IP_20.2.132.137_154
 range 20.2.132.137 20.2.132.154
object network host_20.2.190.2-9
 range 20.2.190.2 20.2.190.9

使用的初始IP地址+末尾IP地址的方式定义IP网段,这些IP网段存在以下转化:

#encoding=utf-8

#base_str为IP网段的开始,一般为形如"192.168.1.1"的字符串
base_str = "192.168.1.1"
#end_str为IP网段的结尾,一般为形如"192.168.1.136"的字符串
end_str = "192.168.1.101"

#base_str,submask_str的int形式,用到了1.1的ip2num
base = ip2num(base_str)        #3232235777
end  = ip2num(widcmask_str)    #3232235877

#IP网段包含的IP地址个数,以及反掩码的int形式
ipnum = end-base+1    #101

#判断某个ip是否在该IP网段中,需要另外写方法
def isIpInSubnetRange(ip,ranges)
    base,end = ranges
    if ip>=base and ip <=end:
        return True
    return False
isIpInSubnetRange(ip2num("192.168.1.100"),(base,end)) #True

2.4 如何对IP网段做合并

在1.3介绍的三种IP网段表现形式中,“初始IP地址+末尾IP地址”(后称“区间”)这种表现方式是最通用的。其他两种表现形式都能等价地转化成一个或多个区间的形式(转化成多个区间一般为使用通配符掩码),但任取一个区间,不一定能很好地转化成其它两种表现形式(可以实现但产生结果比较复杂)。

下面分别对“初始IP地址+子网掩码”、“初始IP地址+通配符掩码”转成“初始IP地址,末尾IP地址”做了代码示例,并提供了区间的合并函数。

def standard2Range(ipobject):
    """
    (初始IP地址,子网掩码)->(初始IP地址,末尾IP地址)
    输入输出全部为int数
    """
    ip,submask = ipobject
    ipnum = (1<<32)-1-submask
    return ip,ip+ipnum

def wildcard2Ranges(ipobject):
    """
    (初始IP地址,通配符掩码)->[(初始IP地址,末尾IP地址)...]
    输入输出全部为int数
    仅提供转化方法,实际转换出来的区间数根据通配符掩码会非常非常多,建议使用wildcard mask的地址不参与整体的合并。
    """
    ip, wildcardmask = ipobject
    wildcardmasksav,icount,nums = wildcardmask, 0,[]
    while wildcardmask!=0:
        if wildcardmask&1==1:
            if icount<0:
                nums.append(icount)
                icount = 1
            else:
                icount+=1
        else:
            if icount>0:
                nums.append(icount)
                icount = -1
            else:
                icount -= 1
        wildcardmask = wildcardmask >> 1
    if icount!=0:
        nums.append(icount)
    baseRange = (0,(1<<nums[0])-1)
    cnt = nums[0]
    multiplyRanges = []
    for it in nums[1:]:
        if it>0:
            rs = (cnt,(1<<it)-1)
            multiplyRanges.append(rs)
        cnt += abs(it)
    lastresult = []
    for rs in multiplyRanges:
        cnt,base =rs
        newresult = []
        if not lastresult:
            for i in range(base + 1):
                lastresult.append([i << cnt])
        else:
            for i in range(base+1):
                for item in lastresult:
                    nt = item+[i<<cnt]
                    newresult.append(nt)
            lastresult = newresult
    base = ip& ((1<<32)-1-wildcardmask)
    res = []
    for it in lastresult:
        start = base + sum(it)
        end = start +  baseRange[1]
        res.append((start,end))
    return res

def sectionInsert(discreteOrNot,subs,*addsubs):
    """
    用来合并subs和addsubs,subs是区间的集合,addsubs也是区间的集合
    discreteOrNot用于区分是否是离散数据,比如 subs=[(1,3)] addsubs=[(4,5)],
    在离散的做法中,3和4这两个数字是连续的,subs和addsubs可以合并成[(1,5)]
    但在连续的做法中,3和4之间还有大量空白,比如3.1、3.2、3.3..,是不连续的,最终结果为[(1,3),(4,5)]
    """
    for addsub in addsubs:
        subs.append(addsub)
    subs=list(set(subs)) #去重
    subs = sorted(subs,key=lambda k:(k[0],k[1]))
    subsextend = []
    for i in range(len(subs)):
        if discreteOrNot:
            subsextend.append([subs[i][0]-0.6, 2 * i])
            subsextend.append([subs[i][1]+0.6, 2 * i + 1])
        else:
            subsextend.append([subs[i][0],2*i])
            subsextend.append([subs[i][1],2*i+1])
    subsextend.sort(key=lambda k:k[0])
    start,end= 0,0
    final = []
    for i in range(len(subsextend)):
        target = subsextend[i]
        if i%2==0 and target[1]==i:
            start = target[0]
        elif i%2==1 and target[1]==i:
            end = target[0]
            if discreteOrNot:
                final.append((int(start+0.6),int(end-0.6)))
            else:
                final.append((start,end))
    return final
    
sectionInsert(True,[(1,3)],(4,5))

2.5 常用协议以及对应的协议号

2.5.1 运行在三层的协议名以及协议号(tcp/ip协议族)

以下为三层采用IP封装的,协议号不同的协议(靠IP头部中的协议字段区分),来自CISCO官网文档。

image.png

2.5.2 运行在四层上的协议名以及对应的协议号

以下为运行在四层TCP/UDP的协议

image.png

2.5.3 常用协议的树状层级

以下为采取树状层级对协议进行归类的一个python代码示例

protoSet = {
    "ah":{
        "v":51,
        "children":{}
    },
    "eigrp":{
        "v":88,
        "children":{}
    },
    "esp":{
        "v":50,
        "children":{}
    },
    "gre":{
        "v":47,
        "children":{}
    },
    "icmp":{
        "v":1,
        "children":{
            "echo-reply":  { "v":0},
            "unreachable":  { "v":3},
            "source-quench":  { "v":4},
            "redirect":  { "v":5},
            "alternate-address":  { "v":6},
            "echo":  { "v":8},
            "router-advertisement":  { "v":9},
            "router-solicitation":  { "v":10},
            "time-exceeded":  { "v":11},
            "parameter-problem":  { "v":12},
            "timestamp-request":  { "v":13},
            "timestamp-reply":  { "v":14},
            "information-request":  { "v":15},
            "information-reply":  { "v":16},
            "mask-request":  { "v":17},
            "mask-reply":  { "v":18},
            "traceroute":  { "v":30},
            "conversion-error":  { "v":31},
            "mobile-redirect":  { "v":32},
        }
    },
    "icmp6":{
        "v":58,
        "children":{}
    },
    "igmp":{
        "v":2,
        "children":{}
    },
    "igrp":{
        "v":9,
        "children":{}
    },
    "ip":{
        "v":0,
        "children":{}
    },
    "ipinip":{
        "v":4,
        "children":{}
    },
    "ipsec":{
        "v":50,
        "children":{}
    },
    "nos":{
        "v":94,
        "children":{}
    },
    "ospf":{
        "v":89,
        "children":{}
    },
    "pcp":{
        "v":108,
        "children":{}
    },
    "pim":{
        "v":103,
        "children":{}
    },
    "pptp":{
        "v":47,
        "children":{}
    },
    "snp":{
        "v":109,
        "children":{}
    },
    "tcp":{
        "v":6,
        "children":{
            "aol": { "v":5190},
            "bgp": { "v":179},
            "chargen": { "v":19},
            "cifs": { "v":3020},
            "citrix-ica": { "v":1494},
            "cmd": { "v":514},
            "ctiqbe": { "v":2748},
            "daytime": { "v":13},
            "discard": { "v":9},
            "domain": { "v":53},
            "echo": { "v":7},
            "exec": { "v":512},
            "finger": { "v":79},
            "ftp": { "v":21},
            "ftp-data": { "v":20},
            "gopher": { "v":70},
            "h323": { "v":1720},
            "hostname": { "v":101},
            "http": { "v":80},
            "https": { "v":443},
            "ident": { "v":113},
            "imap4": { "v":143},
            "irc": { "v":194},
            "kerberos": { "v":750},
            "klogin": { "v":543},
            "kshell": { "v":544},
            "ldap": { "v":389},
            "ldaps": { "v":636},
            "login": { "v":513},
            "lotusnotes": { "v":1352},
            "lpd": { "v":515},
            "netbios-ssn": { "v":139},
            "nfs": { "v":2049},
            "nntp": { "v":119},
            "pcanywhere-data": { "v":5631},
            "pim-auto-rp": { "v":496},
            "pop2": { "v":109},
            "pop3": { "v":110},
            "pptp": { "v":1723},
            "rsh": { "v":514},
            "rtsp": { "v":554},
            "sip": { "v":5060},
            "smtp": { "v":25},
            "sqlnet": { "v":1521},
            "ssh": { "v":22},
            "sunrpc": { "v":111},
            "tacacs": { "v":49},
            "talk": { "v":517},
            "telnet": { "v":23},
            "uucp": { "v":540},
            "whois": { "v":43},
            "www": { "v":80}
        }
    },
    "udp":{
        "v":17,
        "children":{
            "biff": {"v":512},
            "bootpc": {"v":68},
            "bootps": {"v":67},
            "cifs": {"v":3020},
            "discard": {"v":9},
            "dnsix": {"v":195},
            "domain": {"v":53},
            "echo": {"v":7},
            "http": {"v":80},
            "isakmp": {"v":500},
            "kerberos": {"v":750},
            "mobile-ip": {"v":434},
            "nameserver": {"v":42},
            "netbios-dgm": {"v":138},
            "netbios-ns": {"v":137},
            "nfs": {"v":2049},
            "ntp": {"v":123},
            "pcanywhere-status": {"v":5632},
            "pim-auto-rp": {"v":496},
            "radius": {"v":1645},
            "radius-acct": {"v":1646},
            "rip": {"v":520},
            "secureid-udp": {"v":5510},
            "sip": {"v":5060},
            "snmp": {"v":161},
            "snmptrap": {"v":162},
            "sunrpc": {"v":111},
            "syslog": {"v":514},
            "tacacs": {"v":49},
            "talk": {"v":517},
            "tftp": {"v":69},
            "time": {"v":37},
            "vxlan": {"v":4789},
            "who": {"v":513},
            "www": {"v":80},
            "xdmcp": {"v":177},
        }
    }
}

2.6 思科ASA防火墙策略格式

2.6.1 object的配置格式

以下是ASA防火墙中object的定义方式的分类整理(暂不包含wildcardmask的情况)。 image.png

2.6.2 object-group的配置格式

以下是ASA防火墙中object-group的定义方式的分类整理 image.png

2.6.3 access-list的配置格式

以下是ASA防火墙中access-list的定义方式的分类,共分为标准和拓展两种 image.png 因上图没有对acl规则进行详细描述,附上用于匹配的正则表达式作为补充。

# 对标准acl进行匹配,如access-list out permit tcp any host 192.168.0.10 eq http
aclstandard = re.compile(r'^access-list ([^\s]+) (permit|deny) ([^\s]+) '
                         r'(any|host [0-9.]+|subnet [0-9. ]+|range [0-9. ]+) '
                         r'(any|host [0-9.]+|subnet [0-9. ]+|range [0-9. ]+)'
                         r'( eq [^\s]+)?')
# 对拓展acl进行匹配,如access-list Web_access_in extended permit object TCP8888 host 10.2.101.13 object 10.2.94.13
aclextend = re.compile(r'^access-list ([\w\W]+) extended (permit|deny) '
                       r'(any|object [^\s]+|object-group [^\s]+|[^\s]+) '
                       r'(any|host [0-9.]+|subnet [0-9. ]+|range [0-9. ]+|object [^\s]+|object-group [^\s]+) '
                       r'(any|host [0-9.]+|subnet [0-9. ]+|range [0-9. ]+|object [^\s]+|object-group [^\s]+)'
                       r'( eq [^\s]+)?')

2.6.4 NAT的配置格式

本文暂不对NAT配置进行分类,后续会有nat的专题进行讲解。

2.7 Juniper防火墙策略格式

2.7.1 address-book的配置格式

以下为Juniper防火墙中address-book的整理分类示例(暂不包含wildcardmask的情况)。

#初始IP地址/短子网掩码
set security zones security-zone INTERNET address-book address 135.224.178.133/32 35.224.178.133/32
set security zones security-zone INTERNET address-book address 121.7.106.0/24 111.7.106.0/24

#初始IP地址/末尾IP地址
set security zones security-zone INTERNET address-book DNS10 range-address 192.168.1.10 to 192.168.1.100 

#地址集,用于嵌套多个地址
set security zones security-zone INTERNET address-book address-set DNSGROUP address DNS10
set security zones security-zone INTERNET address-book address-set DNSGROUP address 135.224.178.133/32 
set security zones security-zone INTERNET address-book address-set DNSGROUP address 121.7.106.0/24

2.7.2 application的配置格式

以下为juniper防火墙中application的整理分类示例。

#指定单个端口
set applications application TCP8102 protocol tcp
set applications application TCP8102 destination-port 8102

#指定多个端口
set applications application tcp12140-12144 protocol tcp
set applications application tcp12140-12144 destination-port 12140-12144

#指定应用集
set applications application-set ChinaPay-Port application tcp12140-12144
set applications application-set ChinaPay-Port application TCP8102

2.7.3 security policy的配置格式

以下为juniper防火墙中policy的整理分类示例。

#通用的配置格式需要包含source-address,destination-address,application,action(then)四部分  
set security policies from-zone exd to-zone exb policy 3771 match source-address 202.104.148.138/32
set security policies from-zone exd to-zone exb policy 3771 match destination-address 10.14.196.83/32
set security policies from-zone exd to-zone exb policy 3771 match application junos-icmp-all
set security policies from-zone exd to-zone exb policy 3771 match application tcp32003
set security policies from-zone exd to-zone exb policy 3771 then permit

2.7.4 NAT的配置格式

本文暂不对NAT配置进行分类,后续会有nat的专题进行讲解。

2.8 H3C防火墙策略格式

2.8.1 object-group的配置格式

以下为H3C防火墙中object-group的整理分类示例。

#以下演示了三种IP网段的定义形式,都可以放在object-groupobject-group ip address Yesnet
 security-zone Untrust
 0 network subnet 45.51.32.0 255.255.255.0
 10 network range 70.67.45.11 70.67.45.12
 20 network subnet 15.48.128.0 wildcard 0.15.126.255
 30 network subnet 15.48.64.64 wildcard 0.15.31.63
 
#以下演示了两种服务的定义形式,也可以放在object-groupobject-group service 管理端口
 0 service tcp destination eq 9300
 10 service udp destination range 10162 10181

2.8.2 rule的配置格式

以下为H3C防火墙中rule的示例。

##通用的配置格式需要包含source-ip,destination-ip,service,action四部分  
rule 149 name 某安一账通
 action pass
 source-zone Trust
 destination-zone Untrust
 source-ip 一账通测试机
 destination-ip 一账通入口地址
 service 管理端口
 service 8006
 service http

2.8.3 NAT的配置格式

本文暂不对NAT配置进行分类,后续会有nat的专题进行讲解。

3. 防火墙配置解析代码

3.1 防火墙配置解析需要的输出

一般地,不考虑做NAT的情况,防火墙的配置会包含以下的信息。

输出项名称CISCO防火墙Juniper防火墙H3C防火墙
action
源地址
目地址
源端口xx
目端口
策略名

其中策略名、action、源地址、目标地址、目标端口/协议都会被包含,下面的解析代码也会输出这五项数据。IP地址全部用的是int数的形式

#输出项
#每一行的列表内容分别为策略名称、action、源地址、目标地址、目标端口/协议
#针对源目地址的ip网段,此处不考虑wildcard mask的情况,每一个都为 (开始地址,目标地址-开始地址)
#int数不易人工读取,可以自行将之转化成易读的形式,参考3.2的代码进行转化
['GLOBAL', 'permit', [(167945216, 255), (167945728, 255)], [(167940236, 2)], [('tcp', (24019, 24019))]]
['GLOBAL', 'permit', [(167945767, 1)], [(167927432, 0)], [('tcp', (9195, 9195))]]
['GLOBAL', 'permit', [(167945718, 0)], [(486610688, 255)], [('udp', (514, 514))]]
['GLOBAL', 'permit', [(167945728, 255)], [(487629436, 3)], [('tcp', (50883, 50883))]]

3.2 思科ASA防火墙策略解析代码

参考代码如下,该代码可以将一份ASA防火墙配置解析成3.1中给出的形式。

#encoding=utf-8
import re
import struct
import socket
import os
from collections import defaultdict
from functools import partial

#cisco官网搜集到的协议名到协议号的映射
protoSet={'ah':{'v':51,'children':{}},'eigrp':{'v':88,'children':{}},'esp':{'v':50,'children':{}},'gre':{'v':47,'children':{}},'icmp':{'v':1,'children':{'echo-reply':{'v':0},'unreachable':{'v':3},'source-quench':{'v':4},'redirect':{'v':5},'alternate-address':{'v':6},'echo':{'v':8},'router-advertisement':{'v':9},'router-solicitation':{'v':10},'time-exceeded':{'v':11},'parameter-problem':{'v':12},'timestamp-request':{'v':13},'timestamp-reply':{'v':14},'information-request':{'v':15},'information-reply':{'v':16},'mask-request':{'v':17},'mask-reply':{'v':18},'traceroute':{'v':30},'conversion-error':{'v':31},'mobile-redirect':{'v':32},}},'icmp6':{'v':58,'children':{}},'igmp':{'v':2,'children':{}},'igrp':{'v':9,'children':{}},'ip':{'v':0,'children':{}},'ipinip':{'v':4,'children':{}},'ipsec':{'v':50,'children':{}},'nos':{'v':94,'children':{}},'ospf':{'v':89,'children':{}},'pcp':{'v':108,'children':{}},'pim':{'v':103,'children':{}},'pptp':{'v':47,'children':{}},'snp':{'v':109,'children':{}},'tcp':{'v':6,'children':{'aol':{'v':5190},'bgp':{'v':179},'chargen':{'v':19},'cifs':{'v':3020},'citrix-ica':{'v':1494},'cmd':{'v':514},'ctiqbe':{'v':2748},'daytime':{'v':13},'discard':{'v':9},'domain':{'v':53},'echo':{'v':7},'exec':{'v':512},'finger':{'v':79},'ftp':{'v':21},'ftp-data':{'v':20},'gopher':{'v':70},'h323':{'v':1720},'hostname':{'v':101},'http':{'v':80},'https':{'v':443},'ident':{'v':113},'imap4':{'v':143},'irc':{'v':194},'kerberos':{'v':750},'klogin':{'v':543},'kshell':{'v':544},'ldap':{'v':389},'ldaps':{'v':636},'login':{'v':513},'lotusnotes':{'v':1352},'lpd':{'v':515},'netbios-ssn':{'v':139},'nfs':{'v':2049},'nntp':{'v':119},'pcanywhere-data':{'v':5631},'pim-auto-rp':{'v':496},'pop2':{'v':109},'pop3':{'v':110},'pptp':{'v':1723},'rsh':{'v':514},'rtsp':{'v':554},'sip':{'v':5060},'smtp':{'v':25},'sqlnet':{'v':1521},'ssh':{'v':22},'sunrpc':{'v':111},'tacacs':{'v':49},'talk':{'v':517},'telnet':{'v':23},'uucp':{'v':540},'whois':{'v':43},'www':{'v':80}}},'udp':{'v':17,'children':{'biff':{'v':512},'bootpc':{'v':68},'bootps':{'v':67},'cifs':{'v':3020},'discard':{'v':9},'dnsix':{'v':195},'domain':{'v':53},'echo':{'v':7},'http':{'v':80},'isakmp':{'v':500},'kerberos':{'v':750},'mobile-ip':{'v':434},'nameserver':{'v':42},'netbios-dgm':{'v':138},'netbios-ns':{'v':137},'nfs':{'v':2049},'ntp':{'v':123},'pcanywhere-status':{'v':5632},'pim-auto-rp':{'v':496},'radius':{'v':1645},'radius-acct':{'v':1646},'rip':{'v':520},'secureid-udp':{'v':5510},'sip':{'v':5060},'snmp':{'v':161},'snmptrap':{'v':162},'sunrpc':{'v':111},'syslog':{'v':514},'tacacs':{'v':49},'talk':{'v':517},'tftp':{'v':69},'time':{'v':37},'vxlan':{'v':4789},'who':{'v':513},'www':{'v':80},'xdmcp':{'v':177},}}}

# 简单对1.1.1.1这种ip做匹配
ip_host_regex =re.compile(r'\s*[0-9]+.[0-9]+.[0-9]+.[0-9]+\s*$')

# 简单对1.1.1.1 255.255.255.255这种ip做匹配
ip_subnet_regex =re.compile(r'\s*[0-9]+.[0-9]+.[0-9]+.[0-9]+\s*[0-9]+.[0-9]+.[0-9]+.[0-9]+\s*$')

# 简单对1.1.1.1/32这种ip做匹配
ip_short_regex =re.compile(r'\s*[0-9]+.[0-9]+.[0-9]+.[0-9]+/[0-9]+\s*$')

#建立短掩码到真实掩码的映射,如31->11111111111111111111111111111110
Short2num = {str(i):(1<<32)-(1<<(32-i)) for i in range(1,33)}

# 对标准acl进行匹配,如access-list out permit tcp any host 192.168.0.10 eq http
aclstandard = re.compile(r'^access-list ([^\s]+) (permit|deny) ([^\s]+) '
                         r'(any|host [0-9.]+|subnet [0-9. ]+|range [0-9. ]+) '
                         r'(any|host [0-9.]+|subnet [0-9. ]+|range [0-9. ]+)'
                         r'( eq [^\s]+)?')
# 对拓展acl进行匹配,如access-list Web_access_in extended permit object TCP8888 host 10.2.101.13 object 10.2.94.13
aclextend = re.compile(r'^access-list ([\w\W]+) extended (permit|deny) '
                       r'(any|object [^\s]+|object-group [^\s]+|[^\s]+) '
                       r'(any|host [0-9.]+|subnet [0-9. ]+|range [0-9. ]+|object [^\s]+|object-group [^\s]+) '
                       r'(any|host [0-9.]+|subnet [0-9. ]+|range [0-9. ]+|object [^\s]+|object-group [^\s]+)'
                       r'( eq [^\s]+)?')

#object语句的开头,用来判断是否是object/object-group语句的有效组成成分
ObjectConfigStart=["object", "host", "subnet", "range", "service", "object-group", "network-object", "protocol-object", "icmp-object",
 "port-object", "service-object", "group-object"]

def ip2num(ip):
    """将点分十进制ip转化成int数"""
    return struct.unpack("!L", socket.inet_aton(ip))[0]

def num2ip(num):
    """将int数转化成对应的点分十进制ip"""
    a, c, b, d = (num & 0xff000000) >> 24, (num & 0x0000ff00) >> 8, (num & 0x00ff0000) >> 16, (num & 0x000000ff) >> 0
    return "{}.{}.{}.{}".format(a, b, c, d)

def short2num(short):
    """短掩码转化成int格式的长掩码"""
    short = str(short)
    return Short2num[short]

def ipstr2num(ipstr):
    """将字符串形式的 ip、掩码 转化为两个int数 (开始,结束)"""
    if ip_host_regex.match(ipstr): #形如"192.168.1.1"格式进行转化,因为配置中的ip一般都无误,故编写正则表达式较简单,如用于校验请另外编写正则
        return ip2num(ipstr.strip()),0
    elif ip_subnet_regex.match(ipstr):  #形如"192.168.1.1 255.255.255.0"格式进行转化
        ip, subnet = ipstr.strip().split()
        ipn, shortn = ip2num(ip), ip2num(subnet)
        return ipn&shortn,short2num(32)-shortn
    elif ip_short_regex.match(ipstr):  #形如"192.168.1.1/31"格式进行转化
        ip, short = ipstr.split("/")
        ipn, shortn = ip2num(ip),short2num(short)
        return ipn&shortn,short2num(32)-shortn
    print("ipstr2num: '{}' can't be trans".format(ipstr))

def protoTransGetNum(proto3layer,key):
    """获取某一协议名对应的协议号"""
    if key in proto3layer["children"]:
        num = proto3layer["children"][key]["v"]
    else:
        num = int(key)
    return num

def protoTrans(cmdstr):
    """对形如以下的配置做解析:tcp destination range 6600 6699,tcp eq telnet,esp"""
    if cmdstr=="any":
        return "ip",(0,0)
    info = cmdstr.strip().split()
    if "destination" in info:
        info.remove("destination")
    proto3 = info[0]
    proto3layer = protoSet[info[0]]
    #protoNumIn3LayerHeader = proto3layer["v"]
    if len(info) >= 3:
        if info[1] == "eq":
            num = protoTransGetNum(proto3layer,info[2])
            return proto3,(num,num)
        elif info[1]=="lt": #有些防火墙的lt是<=,有些防火墙的lt是<,此处默认为<
            base = protoTransGetNum(proto3layer,info[2])
            return proto3,(0,base-1)
        elif info[1] == "lte":
            base = protoTransGetNum(proto3layer,info[2])
            return proto3,(0, base)
        elif info[1] == "gt":
            base = protoTransGetNum(proto3layer,info[2])
            return proto3,(base+1, 65535)
        elif info[1] == "gte":
            base = protoTransGetNum(proto3layer,info[2])
            return proto3,(base, 65535)
        elif info[1] == "range":
            if len(info)>=4:
                base,end = protoTransGetNum(proto3layer,info[2]),protoTransGetNum(proto3layer,info[3])
                return proto3, (base,end)
        print("protoTrans: '{}' can't be trans".format(cmdstr))
        return None
    else:
        if proto3=="tcp" or proto3=="udp":
            return proto3, (0, 65535)
        return proto3,(0,0)

def addressTrans(cmdstr):
    """对形如以下的配置做解析:host 202.100.1.1,subnet 202.100.1.0 255.255.255.0,range 202.100.2.10 202.100.2.20"""
    if cmdstr=="any":
        return 0,1<<32-1
    info = cmdstr.strip().split()
    if len(info)>=2:
        if info[0]=="range":
            if len(info)>=3:
                base,_ = ipstr2num(info[1])
                end, _  =  ipstr2num(info[2])
                return base,end-base
        else:
            cstr = cmdstr.replace("host","").replace("subnet","").strip()
            return ipstr2num(cstr)
    print("addressTrans: '{}' can't be trans".format(cmdstr))

def sectionInsert(discreteOrNot,subs,*addsubs):
    """
    用来合并subs和addsubs,subs是区间的集合,addsubs也是区间的集合
    discreteOrNot用于区分是否是离散数据,比如 subs=[(1,3)] addsubs=[(4,5)],
    在离散的做法中,3和4这两个数字是连续的,subs和addsubs可以合并成[(1,5)]
    但在连续的做法中,3和4之间还有大量空白,比如3.1、3.2、3.3..,是不连续的,最终结果为[(1,3),(4,5)]
    """
    for addsub in addsubs:
        subs.append(addsub)
    subs=list(set(subs)) #去重
    subs = sorted(subs,key=lambda k:(k[0],k[1]))
    subsextend = []
    for i in range(len(subs)):
        if discreteOrNot:
            subsextend.append([subs[i][0]-0.6, 2 * i])
            subsextend.append([subs[i][1]+0.6, 2 * i + 1])
        else:
            subsextend.append([subs[i][0],2*i])
            subsextend.append([subs[i][1],2*i+1])
    subsextend.sort(key=lambda k:k[0])
    start,end= 0,0
    final = []
    for i in range(len(subsextend)):
        target = subsextend[i]
        if i%2==0 and target[1]==i:
            start = target[0]
        elif i%2==1 and target[1]==i:
            end = target[0]
            if discreteOrNot:
                final.append((int(start+0.6),int(end-0.6)))
            else:
                final.append((start,end))
    return final

class ObjectBlock(object):
    """用来存放一个object/object-group配置块,并且对其解析,产生对应的数据,可以自行调用里面的属性做更多的操作(如压缩配置)"""
    def __init__(self,firstline):
        """用object-group network DM (第一行)这样的配置来初始化配置块"""
        self.config = [firstline]  #用来存放一般的配置语句
        self.quoteconfig = []  #用来存放引用到其他object/object-group的配置语句,完整的config=config+quoteconfig
        self.fulldata = []   #用来存放完整的数据
        self.simpleconfig = [] #用来存放简化的配置
        firstSplit= firstline.strip().split()
        self.FirstType = firstSplit[0]  #"object"或"object-group"
        self.SecondType = firstSplit[1] #"network","service","protocol"等
        self.name = firstSplit[2]       #配置块的名称,如"DM"
        self.extra = ""     #取出形似"object-group service udp.ser udp"中的"udp"
        self.data = []      #本配置块的数据,如"network-object host 10.2.104.96"里的"host 10.2.104.96"会转换成(base,offset)这种形式存在此中。
        self.quote = []     #本配置块引用到的其他的object/object-group名称
        self.quotedata = [] #本配置块引用到的数据
        if len(firstSplit)>3: 
            self.extra = firstSplit[3]
        self.discreteInsert = partial(sectionInsert,True)     #离散数据插入函数
        self.continuousInsert = partial(sectionInsert, False) #连续数据插入函数
        self.usedCount = 0  #本object/object-group被引用次数

    def __repr__(self):
        """返回打印出来的值"""
        return "{}/{}/{}".format(self.FirstType,self.SecondType,self.name)

    def dataMerge(self,*datas):
        """本地数据的归并,可以输入的data有self.data,self.quotedata"""
        originaldata = []
        tmpdata = []
        for data in datas:
            originaldata.extend(data)
        if self.SecondType=="network":
            finaldata = []
            for d in originaldata:
                if isinstance(d,tuple):
                    tmpdata.append((d[0],d[0]+d[1]))
            tmpdata = self.discreteInsert(tmpdata)
            for i in range(len(tmpdata)):
                d = tmpdata[i]
                nd = (d[0],d[1]-d[0])
                finaldata.append(nd)
        else:
            finaldata = []
            tmpdict = defaultdict(list)
            for d in originaldata:
                if isinstance(d,tuple):
                    protoName,r = d
                    tmpdict[protoName] = self.discreteInsert(tmpdict[protoName],(r[0],r[1]))
            for k in tmpdict:
                for i in range(len(tmpdict[k])):
                    d = tmpdict[k][i]
                    nd = (d[0], d[1])
                    finaldata.append((k,nd))
        return finaldata

    def objectCheck(self,liner):
        """检查配置块下面有没有嵌套的object或object-group"""
        if len(liner)>=2:
            if liner[0]=="group-object":
                return "/".join(("object-group",self.SecondType,liner[1]))
        if len(liner)>=3:
            if liner[1]=="object":
                return "/".join(("object",self.SecondType,liner[2]))
        return None

    def addLine(self,line):
        """添加一行如network-object object APP到配置块,并计算这一行引入的数据,添加到self.data"""
        liner = line.strip().split()
        if not liner:
            return
        key = "{}|{}|{}".format(self.FirstType,self.SecondType,liner[0])
        rets = self.objectCheck(liner)
        if rets:
            self.quote.append(rets)
            self.quoteconfig.append(line.strip())
            return
        self.config.append(line.strip())
        if key=="object-group|protocal|protocol-object":
            cmdstr = "".join(liner[1:])
            ret = protoTrans(cmdstr)
            self.data.append(ret)
        elif key=="object-group|service|port-object" or key=="object-group|service|service-object":
            if self.extra:
                cmdstr = "{} {}".format(self.extra," ".join(liner[1:]))
                ret = protoTrans(cmdstr)
                self.data.append(ret)
            else:
                cmdstr = " ".join(liner[1:])
                ret = protoTrans(cmdstr)
                self.data.append(ret)
        elif key=="object-group|icmp-type|icmp-object":
            cmdstr = "icmp eq {}".format(" ".join(liner[1:]))
            ret = protoTrans(cmdstr)
            self.data.append(ret)
        elif key=="object-group|network|network-object":
            cmdstr = " ".join(liner[1:])
            ret = addressTrans(cmdstr)
            self.data.append(ret)
        elif "object|network|" in key:
            if liner[0]=="host" or liner[0]=="subnet" or liner[0]=="range":
                cmdstr = line.strip()
                ret = addressTrans(cmdstr)
                self.data.append(ret)
        elif key=="object|service|service":
            cmdstr = " ".join(liner[1:])
            ret = protoTrans(cmdstr)
            self.data.append(ret)

class ObjectBlockOperation(object):
    """ObjectBlock的操作函数,写到一起方便查阅"""
    @classmethod
    def objectTraverse(cls,nowblock, blocks):
        """用来递归获取quote数据,避免因嵌套使用object-group造成的数据遗漏"""
        if not nowblock.quote:
            return nowblock.data
        retdata = []
        if not nowblock.quotedata:
            if nowblock.quote:
                for quo in nowblock.quote:
                    data = cls.objectTraverse(blocks[quo], blocks)
                    if data:
                        retdata.extend(data)
                return retdata
            return []
        return nowblock.quotedata

    @classmethod
    def readObjects(cls,lines):
        """用来读取所有objct/object-group配置,返回的是Block对象的集合"""
        currentBlock = None  #当前配置块
        Blocks = {}          #所有配置块的字典
        Released = True
        for l in lines:  #对于每行配置
            ler = l.split()
            if not ler or ler[0] not in ObjectConfigStart: #如果不是ObjectConfigStart中的作为开头,说明不是目标配置,直接跳出
                continue
            if l.startswith("object"):  #如果以"object"开头,说明是目标配置的第一行
                if currentBlock:        #如果已有上个配置块(吸收了足够的配置),就将这个代码块存入字典
                    Blocks[repr(currentBlock)] = currentBlock
                    Released = True
                currentBlock = ObjectBlock(l)  #用这行配置新建一个配置块
                continue
            if currentBlock:
                currentBlock.addLine(l)   #往当前配置块添加配置(这个操作会重复多次,直到下次遇到以"object"开头,再新建一个配置块)
                Released = False
        if not Released:  #将最后一个配置块放入字典
            Blocks[repr(currentBlock)] = currentBlock
        names = [k for k in Blocks]
        for name in names:
            nowblock = Blocks[name]
            if nowblock.quote:
                for quo in nowblock.quote:
                    Blocks[quo].usedCount +=1
                quetodata = cls.objectTraverse(nowblock,Blocks) #遍历获得引用的所有数据
                Blocks[name].quotedata = quetodata #赋值给原本为空的quotedata
        return Blocks

    @classmethod
    def mergeData(cls,blocks):
        """对block中的每一个block做数据合并"""
        names = [k for k in Blocks]
        for name in names:
            block = blocks[name]
            data =block.dataMerge(block.data,block.quotedata)
            blocks[name].fulldata = data
        return blocks


def aclAnalyze(lines,blocks):
    """拆解ACL语句,这段代码写得比较烂,因为对accest-list的所有组成情况有些不确定,如果有超出aclextend、aclstandard正则的情况请自行修改正则"""
    names = [k for k in Blocks]
    result = []
    for l in lines:
        ae = aclextend.findall(l.strip())
        if ae:
            pname,action,service,sour,dest,serviceadd = ae[0]
            if serviceadd:
                service = [protoTrans("{} {}".format(service, serviceadd))]
            elif "object" in service or "object-group" in service:
                se = service.split()
                key = ""
                for name in names:
                    if se[1] in name and "/network/" not in name:
                        key = name
                        break
                service = blocks[key].fulldata
            else:
                s = protoTrans(service)
                service = [s]
            if "object" in sour or "object-group" in sour:
                so = sour.split()
                key = ""
                for name in names:
                    if so[1] in name and "/network/" in name:
                        key = name
                        break
                sour = blocks[key].fulldata
            else:
                s = addressTrans(sour)
                sour = [s]
            if "object" in dest or "object-group" in dest:
                de = dest.split()
                key = ""
                for name in names:
                    if de[1] in name and "/network/" in name:
                        key = name
                        break
                dest = blocks[key].fulldata
            else:
                s = addressTrans(dest)
                dest = [s]
            result.append([pname, action, sour, dest,service])
        else:
            ast = aclstandard.findall(l.strip())
            if ast:
                pname, action, service, sour, dest ,serviceadd= ast[0]
                service=[protoTrans("{} {}".format(service,serviceadd))]
                sour = [addressTrans(sour)]
                dest = [addressTrans(dest)]
                result.append([pname, action,  sour, dest, service])
    return result

if __name__ == '__main__':
    with open('{}/asaconfcopy'.format(os.getcwd()),'r') as f: #请于此处修改配置文件名
        ls=f.readlines()
        OBO = ObjectBlockOperation()   #创建操作类,后续可以调用里面的函数
        Blocks = OBO.readObjects(ls)   #读入object/object-group
        Blocks = OBO.mergeData(Blocks) #对现有的数据进行整理合并(主要是有一些重合,将之消除)
        Result = aclAnalyze(ls,Blocks) #用Blocks去对所有acl进行解析,得出解析结果
        for s in Result:
            print(s)

3.3 Juniper防火墙策略解析代码

Juniper的策略结构比较简单易懂,可以参考2.7自行进行解析,参考代码后续补充。

3.4 H3C防火墙策略解析代码

H3C的策略结构在思科的基础上进行了简化,只使用了object-group,可以参考2.8自行进行解析,参考代码后续补充。