搭建一个 802.1x 的 web 测试服务

前言

802.1x 是一种二层认证协议,结合 EAP,它能够为无线网络提供安全的,无感知的认证服务。因此许多大型的园区网都选择使用 802.1x 作为无线网络的认证模式。
大名鼎鼎的 eduroam 也是完全基于 802.1x 认证的全球无线漫游网络。
由于 802.1x 的特性,在进行基于 802.1x 的无线认证时,对终端本身有一定的要求。因此 802.1x 的无线网络排障就比较讨厌,因为其中有相当一部分属于终端侧的配置或兼容问题。
对于 eduroam 漫游时就更加麻烦了,管理员很难确认到底是漫游所在地的 eduroam-sp 存在问题,还是账号所在地的 eduroam-idp 认证存在异常。
因此,最好能有一个基于 web 的 802.1x 认证测试服务,管理员测试一下就知道是哪边出的问题。
这样的服务国外有学校提供,例如这个 Masarykova univerzita
现在我们来自己实现一个

轮子

要做这件事,核心的功能就是要构造模拟的认证报文,并且需要结合
EAP。考虑到普遍性,最先应该支持的应该是 mschapv2 这个。所以当然是先找轮子。。。
https://github.com/Bonn93/radius-python-sanity-check
找到了轮子,测试可以进行 mschapv2 的验证,剩下的事情就好办了,在轮子的基础上封装就好了

需求

先整理一下需求:

  • 我们需要一个 web 表单,让用户输入他的用户名和密码进行测试,并返
    回结果
  • 不同的 SSID 应该能生成对应不同的表单页面
  • 得加一个验证码来防止坏人猜密码
  • 最好再有个 API 可供程序调用,这样可以构建自动化的监控
  • API 得有访问控制

实现

因为轮子是 Python 的,所以整个服务肯定也就用 Python 来实现了,用 flask 来做。下面做一些简单的介绍,完整代码见 GITHUB
目录结构如下:

├── app
│   ├── framd.ttf  #字体文件,用来生成验证码的
│   ├── __init__.py
│   ├── static # 静态文件,css,js 之类的
│   ├── templates # web 模板 
│   ├── utils.py 
│   └── views.py 
├── config.py # 配置文件
├── control # 启动脚本
├── gunicorn.conf # gunicorn 的配置文件
├── lib # radius 报文的封装 lib(就是我们找的那个轮子)
│   ├── bidict.py
│   ├── client.py
│   ├── dictfile.py
│   ├── dictionary.py
│   ├── dicts
│   ├── host.py
│   ├── __init__.py
│   ├── mschap2.py
│   ├── packet.py
│   └── tools.py
├── LICENSE
├── README-CN.md
├── README.md
├── requirement.txt
├── run.py
└── var
    └── app.log

先从 config.py 说起,我们的配置都放在这儿


# BASIC APP CONFIG
BIND_ADDRESS = '0.0.0.0'
PORT = 81
SECRET_KEY = "session_secret_key"  # 建立 session 所使用的 key,session 用来存验证码

# SSID CONFIG
SSID_CONFIG = {
        "test1x":
            {"RADIUS_HOST":"192.168.0.210","RADIUS_SECRET":"802.1x","RADIUS_PORT":1812,"NAS_IP":"192.168.80.5"},
        "eduroam":
            {"RADIUS_HOST":"192.168.0.220","RADIUS_SECRET":"eduroam","RADIUS_PORT":1812,"NAS_IP":"192.168.80.5"},
        }
# 字典的 key 将作为 url 的路径,例如 https://test.edu.cn/test/eduroam

# API_KEY
API_KEY = "0c8d964e8fbd4cfcd040b5691d119968"

因为我们的无线网络可能有多个 SSID,并且不同的 SSID 对应的后端是不同的 Radius。因此需要根据每个 SSID 配置不同的 Radius 配置。这里的 SSID ,同时也会成为最后生成的测试 URL,例如 https://test.example.cn/test/eduroam

挑战报文

def radius_challenge(username, password, host, secret, port, nasip, debug):
        hostname = gethostname()
        dict_path = sys.path[0] + "/lib/dicts/dictionary"
        radius = Client(server = host, secret = secret, authport = port, dict = Dictionary(dict_path))
        request = radius.CreateAuthPacket(code = packet.AccessRequest)
        if debug:
                print "[DEBUG] assembling packet attributes"
        request["User-Name"] = username
        request["NAS-IP-Address"] = nasip
        request["NAS-Identifier"] = hostname
        ……
        ……

我们把轮子里的代码重新封装一下,构造一个函数出来负责发送 Radius 挑战报文。后面就用调用它来做 Radius 的测试

验证码

由于我们提供的是一个开放的测试表单,因此必须要有验证码,否则坏人就要暴力猜密码叻。我们使用 PIL 库进行验证码图像的生成,并把验证码图像对应的数字存在 session 里面。

@app.route('/code', methods=['GET'])
def code():
    """生成验证码
    """
    from io import BytesIO

    output = BytesIO()
    code_img, code_str = create_validate_code() # 生成验证码的函数,在 utils.py 里
    code_img.save(output, 'jpeg')
    img_data=output.getvalue()
    output.close()
    response = make_response(img_data)
    response.headers['Content-Type'] = 'image/jpg'
    session['code_text'] = code_str # 把正确的验证码数字放进 session 里待校验
    return response

表单

@app.route('/test/<string:ssid>', methods=['GET','POST'])
def radius_test(ssid):
        ssid_config = app.config['SSID_CONFIG']
        if ssid not in ssid_config:
                return "404 page not found",404
        if request.method == 'GET':
                return render_template('radius1x.html',ssid=ssid)
        ……
        ……

根据配置不同的 SSID 生成不同的表单 URL,通过校验 session 中的验证码来判断验证码是否正确。然后将收到的用户名和密码发送给 Radius 做测试

API

@app.route('/api/v1/<string:ssid>', methods=['POST'])
def radius1x_api(ssid):
    ssid_config = app.config['SSID_CONFIG']
    if ssid not in ssid_config:
        return "404 page not found",404
        ……
        ……

提供 API 的接口,供程序来调用,通过配置文件中的 API_KEY 进行鉴权

部署

flask 内置了一个 WSGI 服务器,我们测试的时候可以 python run.py 直接跑起来。然而一来这样没法后台运行,二来 flask 内置的 WSGI 性能有点寒碜的,我们得通过其他方法来做生产环境的部署
我们使用 gunicorn 来进行作为生产环境的 WSGI 服务,然后参考 Open-Falcon 里 Dashboard 的控制方式,我们来抄一下它的 Control 脚本~

#!/bin/bash

WORKSPACE=$(cd $(dirname $0)/; pwd)
cd $WORKSPACE

mkdir -p var

module=1xtest
app=radius-$module
pidfile=var/app.pid
logfile=var/app.log

function check_pid() {
    if [ -f $pidfile ];then
        pid=`cat $pidfile`
        if [ -n $pid ]; then
            running=`ps -p $pid|grep -v "PID TTY" |wc -l`
            return $running
        fi
    fi
    return 0
}

function start() {
    source env/bin/activate
    hash gunicorn 2>&- || { echo >&2 "I require gunicorn but it's not installed.  Aborting."; exit 1; }

    check_pid
    running=$?
    if [ $running -gt 0 ];then
        echo -n "$app now is running already, pid="
        cat $pidfile
        return 1
    fi

    gunicorn -c gunicorn.conf run:app -D -t 6000 --pid $pidfile --error-logfile $logfile --log-level info
    sleep 1
    echo -n "$app started..., pid="
    cat $pidfile
}
……
……

这样就可以使用 ./control start,./control stop 来进行服务启停了。

最后再给他套一层 nginx 作为反向代理,把 ssl 证书部上,提供 https 服务。同时再做一个 http 到 https 的重写,这样就差不多了。

运行截图

image.png
[root@host ~]# curl -H "Content-Type: application/json" -d '{"username":"test01@test.edu.cn","password":"test123","token":"0c8d964e8fbd4cfcd040b5691d119968"}' "https://test.edu.cn/api/v1/eduroam"
{
  "result": {
    "method": "mschapv2", 
    "success": true, 
    "time": 0.36426687240600586, 
    "username": "test01@test.edu.cn"
  }
}

联盟监控

eduroam 是一个全球的无线漫游联盟,利用 Radius Proxy 来实现的。进行 eduroam 认证的时候需要讲自己的用户名加上@域名,例如 username@ecnu.edu.cn,eduroam-sp 则将 radius 认证请求转发给上一级节点,上级节点再根据域名转发给对应的节点/或者再向上级转发,和 dns 有点像的。
中国教科网在 2015 年也加入了 eduroam,目前国内有 50 余所高校都提供 eduroam 的全球漫游服务。想想自己的账号跑到其他学校也能直接上网,这确实是件蛮爽的事情。
然而对于 eduroam 的联盟运维而言,联盟规模越大,运维越是麻烦。每个节点出故障的概率如果是0.1%,50 个节点至少有一个出故障的概率就是4.9%,联盟规模越大,出故障的概率越高。对于联盟的运维而言,我们需要能够快速发现故障,快速定位,快速修复。
因此我们可以考虑从每个 eduroam-idp 讨一个测试账号。做一个探针脚本调用 API 进行逐个测试,把测试结果推到监控平台里面。
于是我们就可以完整的监控到每一个 eduroam-idp 的服务是否可用,响应时间,并在宕机的时候及时的发出告警。从而提升整个联盟的运维水平

以上

转载授权

CC BY-SA

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容