基于SDN的访问控制模块实现

Posted 楊木木8023

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于SDN的访问控制模块实现相关的知识,希望对你有一定的参考价值。

一、背景

1、访问控制

访问控制技术,指防止对任何资源进行未授权的访问,从而使计算机系统在合法的范围内使用。意指用户身份及其所归属的某项定义组来限制用户对某些信息项的访问,或限制对某些控制功能的使用的一种技术。访问控制通常是系统管理员控制用户对服务器、目录、文件等网络资源的访问。

2、防火墙

防火墙是最常见的访问控制技术的实现,其通过设置一系列的访问过滤规则实现限制访问主体对客体的访问的功能。现有最常见的防火墙分为:包过滤防火墙和状态检测防火墙。

(1)包过滤防火墙

包过滤是指在网络层对每一个数据包进行检查,根据配置的安全策略转发或丢弃数据包。包过滤防火墙的基本原理是:通过配置访问控制列表(ACL, Access Control List)实施数据包的过滤。主要基于数据包中的源/目的IP地址、源/目的端口号、IP 标识和报文传递的方向等信息。

(2)状态检测防火墙

状态检测是包过滤技术的扩展。基于连接状态的包过滤在进行数据包的检查时,不仅将每个数据包看成是独立单元,还要考虑前后报文的历史关联性。我们知道,所有基于可靠连接的数据流(即基于TCP协议的数据流)的建立都需要经过“客户端同步请求”、“服务器应答”以及“客户端再应答”三个过程(即“三次握手”过程),这说明每个数据包都不是独立存在的,而是前后有着密切的状态联系的。基于这种状态联系,从而发展出状态检测技术。

(3)代理防火墙

代理服务作用于网络的应用层,其实质是把内部网络和外部网络用户之间直接进行的业务由代理接管。代理检查来自用户的请求,用户通过安全策略检查后,该防火墙将代表外部用户与真正的服务器建立连接,转发外部用户请求,并将真正服务器返回的响应回送给外部用户。

3、本模块设计背景

本模块是基于SDN实现的访问控制模块,主要就是实现包过滤防火墙的功能,但是由于包过滤防火墙其静态的ACL规则难以适应动态的安全要求,比如访问权限中,在一些网络中,权限是动态变化的,若使用静态的防火墙规则,其规则的动态变化比较麻烦,所以借助SDN的灵活性,实现的访问控制更具灵活性和适应性。

二、模块设计及框架

1、设计概述

根据背景,我们知道访问控制也就是控制终端对服务器、文件等外部资源的访问获取。也就是说,控制了终端设备对外访问的链路连接也就控制了访问的请求。在基于SDN网络架构的访问控制模块设计中,设计将访问控制量化为四步:信息获取、识别匹配、获取权限、流表下发。

(1)信息获取:如系统模型图所示,实际的访问控制也就是通过控制Ovs来实现主机对外访问,本模块设计采用SDN来实现对ovs流表项的控制。也就是,在交换机和网关路由之间添加SDN交换机进行信息截取,然后将IP地址等信息上传控制器(Ryu),控制器通过北向接口(API)和系统程序进行交互,系统程序从控制器处获取主机信息。

(2)识别:识别主要是在上一步获取的信息基础上根据源ip、目的ip、目的端口等信息进行记录查询,查看是否存在访问权限。

(3)获取权限:在系统程序和数据库进行匹配之后,就可以得出该设备是否有权限访问其想访问的资源,权限包括有、无、异常(返回异常信息)。(因为本系统重在控制主机对资源的获取,通常获取资源主要是利用tcp(http)协议连接,所以本系统抓取TCP协议数据包)

(4)流表下发:在获取权限之后,就需要本系统程序对ovs进行控制,若有权限,则向SDN交换机下发流表项,允许该设备的请求数据转发;若无权限,则向SDN交换机下发流表项,不允许(Drop)该设备的请求数据转发;若权限异常,则向SDN交换机下发流表项,则无处理。

2、系统框架

三、实验topo构建

1、topo构建

本次系统的编写的实验topo是基于mininet进行模拟的,topo如下。

 两个自治域as1、as2

as1: as1-c1对照主机,as1-c2和as1-c3为实验主机,as1-ctrl1为控制器

as2: as2-c1对照主机,as2-c2和as2-c3为实验主机,as2-ctrl2为控制器

test-s1和test-s2为实验的资源服务器

 2、控制器配置

本系统采用的ryu控制器,ryu控制器具有现成的rest_api和switch案例。设置topo时,为了更加合理体现真实网络环境,所以每个自治域都有一个单独的控制器,为了使各个自治域的东西尽可能隔离,本次设计将控制器运行在docker中,本次设计所用docker的镜像和操作参考笔者前期文章:Docker命令、基于Docker的SDN实验环境部署(1)_北风-CSDN博客

首先,创建容器1作为自治域1的控制器的运行环境,其内已经安装了ryu,ip地址为172.17.0.2:

docker run -it --name as1-ryu0 -v /home/ymumu/ryu/ryu/app:/home/ryu/ryu/app ymumu/ryu:0.1 /bin/bash

 在容器内,运行ryu的ofctl_rest.py和simple_switch_13.py

 然后,创建容器2作为自治域2的控制器运行环境,ip地址为172.17.0.3:

docker run -it --name as2-ryu0 -v /home/ymumu/ryu/ryu/app:/home/ryu/ryu/app ymumu/ryu:0.1 /bin/bash

 在容器内,运行ryu的ofctl_rest.py和simple_switch_13.py

 3、路由器ip和路由表配置

要想使得整个网络topo实现正常的网络互通,必须配置好路由器和路由表。

给路由器r8分配ip:

test-s1添加路由表:

test-s2添加路由表:

as1-c1添加路由信息:

as1-c2添加路由信息:

as1-c3添加路由信息:

as2-c1添加路由信息:

as2-c2添加路由信息:

as2-c3添加路由信息:

 四、访问控制技术实现

1、C/S连接信息获取

连接信息获取是访问控制的第一步,主要是通过连接信息的截取分析才可以知道本次连接的源ip、目的ip、目的端口等信息,再通过数据库查询得出权限信息。下面是本系统获取连接信息的方法。

在模拟topo中,我采用tcpdump进行抓包并上传本系统的应用层端(实际应用中,抓包不是这种方式,持续关注,后期更新)。抓包数据即是需要对主机访问服务器的连接进行监控,也就是如本模型系统,as1-c2访问test-s1获取其中的资源,需要http协议进行连接获取,就使用tcpdump抓取数据包并发送给本系统的应用层端,应用层端提取源ip、目的ip、目的端口信息。

抓取信息的代码如下:

"""
利用python实时获取tcpdump的监控数据,并将其发送给服务端:
在网络结构中,主要是监听as1-ovs1-eth2网卡的网络数据信息,也就是监听as1-c2和as1-c3的访问信息。
通过抓包就可以知道服务端到客户端的访问,并通过本系统的处理,就可以实现访问认证。
"""

import subprocess
import socket
import time


class GrabBag:
    def __init__(self, ipaddr, port):
        self.ipaddr = ipaddr
        self.port = port

    def py_tcpdump_as1(self):
        c1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        c1.connect((self.ipaddr, self.port))
        # 抓目的主机为10.0.3.1或者10.0.3.2(两个服务端)的tcp(http)连接,as1-ovs1-eth2是客户端到服务端的必经网卡
        proc = subprocess.Popen('tcpdump dst host 10.0.3.1 or 10.0.3.2 -l -i as1-ovs1-eth2 and tcp -n',
                                stdout=subprocess.PIPE, shell=True)

        while True:
            line = proc.stdout.readline().decode(encoding='utf-8')
            line = line.strip()
            if not line:
                print('tcpdump finished...')
                break
            print(line)
            c1.send(line.encode())
            time.sleep(0.3)

抓包的程序和本系统的应用层端的通信采用tcp socket进行,抓包程序相当于tcp客户端,应用层端的接收程序相当于服务端,服务端采用多进程socket,用来同时接收不同自治域的抓包信息,并进行处理,服务端代码如下:

import socket
import re
from multiprocessing import Process
from app.dataBase_candle import FindAuth
from app.flowTable_operate import PostOperation

"""
tcpdump抓包服务端:
主要用来接收抓包监测的终端访问资源服务器的连接信息,将抓到的包发送到这里进行处理,
得到源ip、目的ip、目的端口等信息,进而去数据库寻找此连接权限,若权限有,则下发相对应的通过流表项

本系统的服务端采用多进程实现socket客户端的多连接,可以处理接收多个抓包程序发送过来的信息。
"""


class Server1:
    def __init__(self, host, port, lst_num):
        self.host = host
        self.port = port
        self.lst_num = lst_num

    # 数据处理
    def recv_message(self, conn, addr):
        data = conn.recv(1024)
        # 对收到的抓包信息进行解析,得出源ip、目的ip、目的端口等信息
        result = re.findall(".*IP (.*): Flags.*", data.decode('utf-8'))
        list_res = result[0].split('> ')
        src_ip = '.'.join(list_res[0].split('.')[:4])
        dst_ip, dst_port = '.'.join(list_res[1].split('.')[0:4]), list_res[1].split('.')[4:]
        # print(src_ip, dst_ip, dst_port[0])

        # 获取权限
        find_auth = FindAuth()
        auth_res = find_auth.get_data(src_ip=src_ip, dst_ip=dst_ip, dst_port=dst_port[0])
        as_res = find_auth.get_as(src_ip=src_ip)
        print("from 0:".format(addr), data.decode('utf-8'))
        print('权限:', auth_res)

        # 根据所属自治域选择向不同的控制器下发流表项
        if as_res == 'as1':
            postTable = PostOperation('172.17.0.2', '8080')
            postTable.post_add_flow(src_ip=src_ip, dst_ip=dst_ip, dst_port=dst_port[0], auth=auth_res)
        elif as_res == 'as2':
            postTable = PostOperation('172.17.0.3', '8080')
            postTable.post_add_flow(src_ip=src_ip, dst_ip=dst_ip, dst_port=dst_port[0], auth=auth_res)

    # 服务端的数据接收,在调用时使用多进程
    def server_link(self, conn, addr):
        conn.send("Welcome connect!".encode())

        while True:
            try:
                self.recv_message(conn, addr)
            except Exception:
                break

        conn.close()

    # 服务端的启动程序
    def server_start(self):
        # IPv4
        s_pro = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 操作系统会在服务器socket被关闭或服务器进程终止后马上释放该服务器的端口
        s_pro.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s_pro.bind((self.host, self.port))
        s_pro.listen(self.lst_num)
        print('Waiting link...')
        while True:
            conn, addr = s_pro.accept()
            print("Success connect from ", addr)
            # 启动多进程实现多连接
            p = Process(target=self.server_link, args=(conn, addr))
            p.start()

注:其实更简单的方式,是不需要抓包,只在数据库记录中权限改变时,本系统就主动下发流表规则,但是之所以不在权限变化时,由应用层下发流表项,而是抓包解析后对比数据库之后再下发,因为网络中可能出现新的数据库记录中没有的主机,这个时候就需要抓包发现,上报系统。

2、数据库设计

(1)、mysql表设计

数据库设计了两个表:一个是权限记录表,一个是主机域归属表。

权限记录表:该表记录各个连接的权限,通过源ip、目的ip、目的端口可以唯一确定一条记录,确定此连接是否有权限进行资源访问,如下图所示。

主机域归属表:该表用来记录各个主机ip对应的域,也就是对应的控制器,在下发流表项时,根据域进行选择控制器,如下图所示。

 MySQL语句如下。

CREATE DATABASE auth_data_db;
USE auth_data_db;

-- 创建权限记录表,记录访问连接的权限,一个访问连接由源ip、目的ip、目的端口确定
CREATE TABLE tb_as(
	rno INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
	src_ip VARCHAR(20) NOT NULL,
	dst_ip VARCHAR(20) NOT NULL,
	dst_port INT NOT NULL,
	acc_auth VARCHAR(10) NOT NULL
);

-- drop table tb_as;
-- delete from tb_as;

ALTER TABLE tb_as AUTO_INCREMENT=1001;

-- 插入权限数据
INSERT INTO tb_as(src_ip, dst_ip, dst_port, acc_auth) VALUES
	('10.0.1.2', '10.0.3.1', 8001, 'yes'),
	('10.0.1.2', '10.0.3.1', 8002, 'yes'),
	('10.0.1.2', '10.0.3.1', 8003, 'no'),
	('10.0.1.3', '10.0.3.1', 8001, 'unknown'),
	('10.0.1.3', '10.0.3.1', 8002, 'no'),
	('10.0.1.3', '10.0.3.1', 8003, 'yes'),
	('10.0.1.2', '10.0.3.2', 8001, 'no'),
	('10.0.1.2', '10.0.3.2', 8002, 'no'),
	('10.0.1.2', '10.0.3.2', 8003, 'unknown'),
	('10.0.1.3', '10.0.3.2', 8001, 'yes'),
	('10.0.1.3', '10.0.3.2', 8002, 'no'),
	('10.0.1.3', '10.0.3.2', 8003, 'yes'),
	('10.0.2.2', '10.0.3.1', 8001, 'no'),
	('10.0.2.2', '10.0.3.1', 8002, 'yes'),
	('10.0.2.2', '10.0.3.1', 8003, 'yes'),
	('10.0.2.3', '10.0.3.1', 8001, 'no'),
	('10.0.2.3', '10.0.3.1', 8002, 'no'),
	('10.0.2.3', '10.0.3.1', 8003, 'yes'),
	('10.0.2.2', '10.0.3.2', 8001, 'unknown'),
	('10.0.2.2', '10.0.3.2', 8002, 'no'),
	('10.0.2.2', '10.0.3.2', 8003, 'no'),
	('10.0.2.3', '10.0.3.2', 8001, 'yes'),
	('10.0.2.3', '10.0.3.2', 8002, 'yes'),
	('10.0.2.3', '10.0.3.2', 8003, 'no');


select acc_auth from tb_as where src_ip='10.0.1.2' and dst_ip='10.0.3.1' and dst_port='8001';


-- 创建所属域表,即记录主机ip地址和其所属自治域
Create table tb_as_ascription(
	ip varchar(20) PRIMARY KEY NOT NULL,
	asc_as varchar(10) not null
);

insert into tb_as_ascription values
	('10.0.1.1', 'as1'),
	('10.0.1.2', 'as1'),
	('10.0.1.3', 'as1'),
	('10.0.2.1', 'as2'),
	('10.0.2.2', 'as2'),
	('10.0.2.3', 'as2');

SELECT asc_as FROM tb_as_ascription WHERE ip='10.0.2.1';


select asc_as from tb_as_ascription WHERE ip is (select src_ip)


-- alter table tb_as AUTO_INCREMENT = 1001;
-- INSERT INTO tb_as(src_ip, dst_ip, dst_port, acc_auth) VALUES
-- 	('10.0.5.2', '10.0.5.1', 8001, 'yes');

-- select count(rno) from tb_as where src_ip='10.0.1.2' and dst_ip='10.0.3.1' and dst_port=8001;

update tb_as SET acc_auth='yes' WHERE src_ip='10.0.1.2' and dst_ip='10.0.3.1' and dst_port=8001;

 (2)、redis表设计

 在查询过程中,由于MySQL的查询速度相对较慢,所以为了不影响连接速度,本系统采用MySQL作为持久化存储数据库,redis作为缓存数据库,用来加快查询速度。在本系统中,这里redis表的设计采用string类型的数据,这样每条记录以key-value对的形式进行存储,以每个连接的源ip、目的ip、目的端口、作为string表的key,权限信息作为string表的value,即一个记录:

set '10.0.1.2:10.0.3.1:8001' 'yes'

同时为了主要防止redis缓存数据过多,清除不常用缓存数据,设置过期时间:

expire'10.0.1.2:10.0.3.1:8001' 300

即过期时间为5分钟。

(3)、数据库记录更新代码

在系统运行过程中,系统管理员可能随时对各个连接权限进行更改,也就是更新数据库记录,由于系统先查询redis缓存,再查询MySQL数据库的记录,所以在更新过程中需要先更新redis的记录,再更新MySQL的的记录,数据库更新程序代码如下。关于redis和MySQL数据一致性的具体讲解可以参见:基于python的MySQL和redis数据同步实现(redis做缓存)_北风-CSDN博客

"""
数据库更新代码:
此部分主要是用来更新权限数据库记录。
更新数据库时,要考虑redis和MySQL的数据一致性问题。所以在更新数据时,先查询redis中是否有此记录,若有,
则先删除redis记录,再插入数据到redis,然后将数据插入MySQL中,插入成功后,重新将数据再次更新到redis。
若redis中无记录,则直接更新MySQL中的记录。
"""

    """
    更新数据的操作,为了避免更新MySQL后,redis没更新的这一段空挡时间的查询,所以先更新redis,
    再更新MySQL,然后MySQL成功提交后,再次对redis进行重新更新
    """

    def post_data(self):
        # 插入数据
        print('输入更新数据:')
        src_ip = input('src_ip:')
        dst_ip = input('dst_ip:')
        dst_port = input('dst_port:')
        auth = input('auth:')
        # redis  string类型表的key
        key = src_ip + ':' + dst_ip + ':' + dst_port

        # 先查询redis数据库是否存在数据,如果存在数据则更新redis,再更新MySQL,若不存在则去MySQL中更新,提交成功再次更新redis
        result = self.r0.get(key)
        # reids存在数据,则需要对数据进行更新,即先清除再写入; 写入redis后,再将数据写入MySQL
        if result:
            # 清除数据
            self.r0.delete(key)
            self.r0.set(key, auth)
            self.r0.expire(key, 600)  # 设置过期时间

            # 先查询MySQL表内是否存在记录,如不存在记录,则插入记录;如存在记录,则修改记录
            count = 0
            with self.conn.cursor() as find_cursor:
                try:
                    count = find_cursor.execute(
                        'select count(rno) from tb_as where src_ip=%s and dst_ip=%s and dst_port=%s',
                        (src_ip, dst_ip, int(dst_port),)
                    )
                except MySQLError as error:
                    print(error)

            with self.conn.cursor() as cursor:
                try:
                    if count == 0:
                        # 插入SQL语句,result为返回的结果
                        res_info = cursor.execute(
                            'insert into tb_student values (%s, %s, %s, %s)',
                            (src_ip, dst_ip, int(dst_port), auth,)
                        )
                    else:
                        # 更新权限数据
                        res_info = cursor.execute(
                            'UPDATE tb_as SET acc_auth=%s WHERE src_ip=%s AND dst_ip=%s AND dst_port=%s',
                            (auth, src_ip, dst_ip, int(dst_port),)
                        )
                    # 成功插入后需要提交才能同步在数据库中
                    if isinstance(res_info, int):
                        print('数据更新成功')
                        self.conn.commit()
                        # 再次更新redis
                        self.r0.set(key, auth)
                        self.r0.expire(key, 600)  # 设置过期时间
                except MySQLError as error:
                    # 如果MySQL提交不成功,清除redis数据
                    self.r0.delete(key)
                    print(error)
                    self.conn.rollback()
                finally:
                    # 操作执行完成后,需要关闭连接
                    self.conn.close()
        else:
            # 先查询MySQL表内是否存在记录,如不存在记录,则插入记录;如存在记录,则修改记录
            count = 0
            with self.conn.cursor() as find_cursor:
                try:
                    count = find_cursor.execute(
                        'select count(rno) from tb_as where src_ip=%s and dst_ip=%s and dst_port=%s',
                        (src_ip, dst_ip, int(dst_port),)
                    )
                except MySQLError as error:
                    print(error)

            with self.conn.cursor() as cursor:
                try:
                    if count == 0:
                        # 插入SQL语句,result为返回的结果
                        res_info = cursor.execute(
                            'insert into tb_student values (%s, %s, %s, %s)',
                            (src_ip, dst_ip, int(dst_port), auth,)
                        )
                    else:
                        # 更新权限数据
                        res_info = cursor.execute(
                            'UPDATE tb_as SET acc_auth=%s WHERE src_ip=%s AND dst_ip=%s AND dst_port=%s',
                            (auth, src_ip, dst_ip, int(dst_port),)
                        )
                    # 成功插入后需要提交才能同步在数据库中
                    if isinstance(res_info, int):
                        print('数据更新成功')
                        self.conn.commit()
                except MySQLError as error:
                    print(error)
                    self.conn.rollback()
                finally:
                    # 操作执行完成后,需要关闭连接
                    self.conn.close()

(4)、权限获取代码

权限的获取,数据库采用MySQL+redis:redis作为缓存,加快查询速度,所以在查询权限数据时,先查询redis数据库中是否存在对应的数据记录,若有,则返回查询数据,若无,则进入MySQL中进行数据查询,并返回结果,同时向redis缓存更新记录。主要代码如下:

# 查询连接权限
    def get_data(self, src_ip, dst_ip, dst_port):
        src_ip, dst_ip, dst_port = str(src_ip), str(dst_ip), str(dst_port)
        # redis string表key
        find_info = src_ip + ':' + dst_ip + ':' + dst_port
        # print(find_info)

        # 先查询redis数据库是否存在数据,如果存在数据则返回输出,若不存在则去MySQL中查询,然后再将结果更新到redis中
        result = self.r0.get(find_info)
        # 结果不为空 即redis存在查询的信息,直接输出信息,否则redis中不存在,需要查询MySQL
        if result:
            """
            每次在redis中更新或者写入数据都需要设置过期时间10分钟,然后每查询到一次就重置过期时间10分钟,
            若10分钟没有查询到这个数据,就会被清除。这样设置过期时间主要防止redis缓存数据过多,清除不常用缓存数据"""
            self.r0.expire(find_info, 600)
            # print(result)
            # 返回查询的权限结果
            return result
        else:
            with self.conn.cursor() as cursor:
                try:
                    # 执行MySQL的查询操作
                    cursor.execute('SELECT acc_auth FROM tb_as WHERE '
                                   'src_ip=%s AND dst_ip=%s AND dst_port=%s', (src_ip, dst_ip, dst_port))
                    result_sql = cursor.fetchall()
                    # print(result_sql)
                    if result_sql:
                        # 将查询结果更新写入redis数据库中
                        auth_res = result_sql[0][0]
                        # print(auth_res)
                        self.r0.set(find_info, auth_res)
                        self.r0.expire(find_info, 600)  # 设置过期时间
                        # 返回查询的权限结果
                        return auth_res
                    else:
                        return 'NULL'
                except Exception as error:
                    print(error)

3、流表下发实现

当系统运行时,首先向交换机下发初始化流表项,也就是drop流表项,对于初始化时,先去获取MySQL数据库中存储的记录,将所有的记录提取出源ip、目的ip、目的端口,也就是初始时全部禁止客户端对服务器资源的获取,下发数据如下:

上述由ryu北向接口向交换机下发,即相当于以下命令:

ovs-ofctl add-flow as1-ovs1 priority=100,tcp,tcp_dst=8001/0xfff0,nw_src=10.0.1.2,nw_dst=10.0.3.1,actions=drop

然后,当终端访问服务器时,由抓包程序拦截的信息传输到本系统,系统通过查询数据库确定访问是否有权限,如有权限,则本系统向交换机下发流表项,这里并不会删除初始化的drop流表项,本系统通过设置更高的优先级,在权限时间内新的权限通过流表项会优先于drop流表项,但是当权限流表项过期后,自动删除,终端访问资源服务器必须要重新取得授权。下发数据如下:

如果权限查询为“no”则,需要删除对应的流表项,有则删除,无则跳过。即,当上次访问时,权限为“yes”,再次访问时间距时间不超过5分钟,下发的流表项并未过期,但是此时间段内数据库的权限已经变更为“no”,所以需要重新更新流表项,也就是需要将通过流表项删除。删除的数据如下:

 五、结论

以上介绍,就是基于SDN的访问控制模块的实现思想,全部代码和网络topo构建详见:

楊mumu/AccessControl_Model

后期,会持续更新和迭代访问控制版本,包括抓取应用层数据包,自动探测域归属等,请各位持续关注。如有问题,请留言。

以上是关于基于SDN的访问控制模块实现的主要内容,如果未能解决你的问题,请参考以下文章

基于SDN的访问控制模块实现

Docker+Ovs构建SDN网络

Docker+Ovs构建SDN网络

Docker+Ovs构建SDN网络

Tungsten Fabric SDN — 基于 Tags 的安全访问控制策略

Tungsten Fabric SDN — 基于 Tags 的安全访问控制策略