用python实现华为PORTAL协议V2
一、什么是PORAL协议Portal协议提供了这样一种方式。当用户未认证时,控制用户只能访问某些特定的网络资源。当用户需要访问互联网更多资源的时候,必须进行认证。它不需要用户安装特定的客户端,只需要通过浏览器,当用户没有认证时,通过HTTP重定向到特定的认证页面,引导用户完成认证的过程。并在此过程中开展广告、社区服务等个性化业务。《华为公司宽带产品Portal协议标准(V2.0)》和《中移动POR
一、什么是PORAL协议
Portal协议提供了这样一种方式。当用户未认证时,控制用户只能访问某些特定的网络资源。当用户需要访问互联网更多资源的时候,必须进行认证。
它不需要用户安装特定的客户端,只需要通过浏览器,当用户没有认证时,通过HTTP重定向到特定的认证页面,引导用户完成认证的过程。并在此过程中开展广告、社区服务等个性化业务。
《华为公司宽带产品Portal协议标准(V2.0)》和《中移动PORTAL协议规范》。这两种标准,在协议上是一脉相承的。
华为的V1.0标准和CMCC的标准基本一样。华为的V2.0标准是在V1.0的标准上稍微有一部分改动。但是引入了报文验证字之后,V2.0标准与V1.0标准完全不兼容。
CMCC的标准还是保持跟华为V1.0标准一致。
本文用PYTHON实现的是华为PORTAL协议V2.0,并且在华为NE40E上测试通过
二、协议交互过程
具体的交互可以参考华为公司宽带产品Portal协议标准(V2.0),(https://wenku.baidu.com/view/df6bb9ffd0f34693daef5ef7ba0d4a7302766c8b.html)这是百度文库里面的,可以免费阅读,但下载需要收费。这里只是简单的说一下:
1、用户访问互联网,BAS当发现用户访问白名单之外的内容时,会把用户的HTTP请求重定向到预先配置的PORTAL WEB服务器。
2、用户的WEB上输入帐号和密码。交点击提交。
3、PORTAL 服务器收到用户的提交后,会按照PORTAL协议跟BAS进行如下交互:
3.1 PORTAL 服务器发送请求挑战REQ_CHALLENGE(CHAP才需要,PAP认证不需要,但本文的实现是使用CHAP的)给BAS。
3.2 BAS根据请求报文里面的用户IP生成相应的challenge,并通过ACK_CHALLENGE发回给PORTAL 服务器
3.3 PORTAL 服务器 接收BAS发过来的ACK_CHALLENGE,并使用该challenge,结合之前收到的用户名和密码,发送认证请求REQ_AUTH给BAS
3.4 BAS收到用户名和密码后,把这些信息发给配置的RADIUS服务器进行认证,并通过ACK_AUTH返回认证结果给PORTAL 服务器
3.5 PORTAL 服务器接收BAS发过来的ACK_AUTH,并发送AFF_ACK_AUTH进行确认
至此PORTAL认证完成,用户可以正常访问互联网。
三、BAS上的配置(以华为NE40E做为示例)
BAS上的配置稍微有点复杂,请参照华为数通的配置参考
//定义IP地址池
ip pool myippool bas local
gateway 2.2.2.1 255.255.255.0
section 0 2.2.2.2 2.2.2.254
dns-server 114.114.114.114
//定义用户组,在白名单里面可以引用这个用户组
user-group wlan
//定义白名单,不需要认证就可以访问的IP
acl number 6100
rule 1 permit ip source user-group wlan destination ip-address 1.1.1.1 0 //允许用户访问PORTAL WEB服务器
rule 1 permit ip source user-group wlan destination ip-address 114.114.114.114 0 //允许用户访问DNS
rule 15 deny ip source user-group wlan destination ip-address 116.8.0.1 0
//定义互联网资源
acl number 6101
rule 15 permit ip source user-group wlan
//定义流分类
traffic classifier wlan_pre_auth operator or
if-match acl 6100
traffic classifier wlan_pre_auth_2 operator or
if-match acl 6101
//定义流动作
traffic behavior http-redirect
http-redirect
traffic behavior permit
//定义全局策略
traffic policy Global_inbound_policy
classifier wlan_pre_auth behavior permit
classifier wlan_pre_auth_2 behavior http-redirect
//应用全局策略
traffic-policy Global_inbound_policy inbound
#
//配置认证前域
domain pre.gl
authentication-scheme default0 //认证前不计费,不认证
accounting-scheme default0
ip-pool myippool //之前配置的地址池
qos-profile B_4M inbound //进行适当的限速
qos-profile B_4M outbound
user-group wlan //配置用户组,这样可以在全局策略里使用基于用户的访问列表进行访问控制
idle-cut 10 1000 //配置空闲策略,以避免用户长期占用地址
web-server 1.1.1.1 //配置PORTAL服务器地址
web-server url http://1.1.1.1:8889 //这里面配置重定向的URL,这个URL的IP一定要在白名单里面,并且可以跟上面的PORTAL服务器地址不一样
http-hostcar enable
//配置全局WEB PORTAL服务
web-auth-server 1.1.1.1 port 50100 key simple yourpass
http-reply exclude destination-ip 1.1.1.1
//配置认证后的域
//先要配置RADIUS服务器
radius-server group myradius.gl
radius-server authentication 1.1.1.2 1812 weight 0
radius-server accounting 1.1.1.2 1813 weight 0
radius-server shared-key yourkey
radius-server source interface LoopBack0
undo radius-server user-name domain-included //带不带域名这个要看RADIUS服务器上的配置,根据自己的实际情况来定
//认证后的域
domain mydomai.gl
ip-pool myippool
radius-server group myradius.gl
//配置用户侧接口,里面涉及到的认证前域,认证域和漫游域,稍微有点复杂,这里不做解释,请自行参考华为数通的配置参考
interface
user-vlan 3691
bas
#
access-type layer2-subscriber default-domain pre-authentication pre.gl authentication mydomai.gl
nas-port-type 802.11
roam-domain mydomai.gl
authentication-method web
#
#
四、具体的python代码
完整的PORTAL系统应该由三个部分组成:
1、WEB服务器,按上面的配置IP是1.1.1.1,监听的端口是TCP 8889,这个IP一定要放到白名单里面,主要用于跟用户交互,获取用户输入的用户名和密码
2、PORTAL服务,按上面的配置是1.1.1.1,监听的端口是UDP 50100,这个IP可以不放在白名单里面(上面的配置WEB和PORTAL服务是同一IP的,其实也可以不同),主要用于使用PORTAL协议跟BAS交互
3、RADIUS服务,按上面的配置是1.1.1.2,认证端口是UDP 1812,记帐端口是UDP 1813
这三个部分可以是用同一个进程实现,也可以使用三个不同的进程实现。在我自己的实现当中是使用三个不同的程序分别实现的。WEB服务器和PORTAL服务需要进行一定的交互,主要是WEB服务器要把用户IP、用户名和密码发给PORTAL服务,PORTAL服务把相应的信息打包发给BAS,并把BAS的认证结果返回给WEB服务,以便WEB服务把认证结果返回给用户。我使用了一个自定义的协议,报文处理就在SimpleMessage这个类里。而PORTAL协议的处理主要在PortalMessage这个类里面。下面的代码主要是PortalMessage这个类,实现了REQ_CHALLENGE、REQ_AUTH和AFF_ACK_AUTH以及REQ_LOGOUT这4种报文,并可以解码ACK_CHALLENGE和ACK_AUTH这两种BAS发过来的报文。基本满足了业务需求。注意以下的代码只是实现了第2部分PORTAL服务中报文协议的编码和解码,其它的事情需要自己编码处理
#-* -coding: utf8 -* -
'''
Created on 2021-6-25
PORTAL协议和自定义协议的类
@author: qinhui
'''
import random
import hashlib
md5_constructor = hashlib.md5
import struct
import binascii
import sys
import datetime
#定义portal报文类型
REQ_CHALLENGE=1
ACK_CHALLENGE=2
REQ_AUTH=3
ACK_AUTH=4
REQ_LOGOUT=5
ACK_LOGOUT=6
AFF_ACK_AUTH=7
NTF_LOGOUT=8
REQ_INFO=9
ACK_INFO=10
#定义属性类型
ATTR_UserNAME=1
ATTR_PassWord=2
ATTR_Challenge=3
ATTR_ChapPassWord=4
ATTR_TextInfo=5
ATTR_UplinkFlux=6
ATTR_DownFlux=7
ATTR_Port=8
#打印二进制字节流
def printBin(data):
s1=binascii.b2a_hex(data).decode('utf-8')
print s1
#myhex=[]
#for x in bytes(data):
# myhex.append(hex(ord(x)))
#print " ".join(myhex)
return s1
#打印日志信息
def printLog(strTitle,strMsg):
now=datetime.datetime.now()
strNow=now.strftime("%Y-%m-%d %H:%M:%S")
print "%s [-] %s->%s" %(strNow,strTitle,strMsg)
#解码TLV数据
def decodeTLV(attNum,data):
data=bytearray(data)
#print "decodeTLV"
#printBin(data)
start=0
dataLen=len(data)
attrList=[]
for _ in range(attNum):
if dataLen-start<3:
break
attType=data[start]
attLen=data[start+1]-2
if dataLen-start<data[start+1]:
break
attData=data[start+2:start+2+attLen]
attrList.append((attType,attLen,attData))
#print (attType,attLen,attData)
start=start+attLen+2
return attrList
#编码TLV
def encodeTLV(attList):
buf=bytearray()
for attType,attLen,attData in attList:
buf.append(attType)
buf.append(attLen+2)
buf=buf+attData
return buf
#根据ErrCode及报文类型返回具体的错误信息
def getErrorInfo(pktType,ErrCode):
errInfo=u"未知错误:%d->%d" % (pktType,ErrCode)
if pktType==ACK_CHALLENGE:
if ErrCode==0:
errInfo=u"请求挑战成功"
elif ErrCode==1:
errInfo=u"请求挑战被拒绝"
elif ErrCode==2:
errInfo=u"链接已建立"
elif ErrCode==3:
errInfo=u"有一个用户在认证过程中,请稍后重试"
elif ErrCode==1:
errInfo=u"请求挑战失败,发生错误"
elif pktType==ACK_AUTH:
if ErrCode==0:
errInfo=u"认证成功"
elif ErrCode==1:
errInfo=u"认证请求被拒绝"
elif ErrCode==2:
errInfo=u"链接已建立"
elif ErrCode==3:
errInfo=u"有一个用户在认证过程中,请稍后重试"
elif ErrCode==1:
errInfo=u"认证请求失败,发生错误"
elif pktType==REQ_LOGOUT:
if ErrCode==0:
errInfo=u"用户下线成功"
elif ErrCode==1:
errInfo=u"没有收到BAS的响应报文"
elif pktType==ACK_LOGOUT:
if ErrCode==0:
errInfo=u"用户下线成功"
elif ErrCode==1:
errInfo=u"用户下线被拒绝"
elif ErrCode==2:
errInfo=u"用户下线失败"
elif pktType==ACK_INFO:
if ErrCode==0:
errInfo=u"处理成功"
elif ErrCode==1:
errInfo=u"功能不支持"
elif ErrCode==2:
errInfo=u"消息处理失败"
#以下为自定义的错误信息
elif pktType==100:
if ErrCode==0:
errInfo=u"处理成功"
elif ErrCode==1:
errInfo=u"认证请求不完整"
elif ErrCode==2:
errInfo=u"消息类型没有定义"
return errInfo
#处理自定义消息简单协议的类,该协议用于WEB服务跟portal服务交互
'''
协议定义如下:
第1和2字节为标志字节分别是十进制88和99
第3字节是报文类型:
1:认证请求
2:注销请求
3:操作成功
4:操作失败
第4字节为TVL数量
从第5字节开始到最后都是TLV字段
TVL的类型定义如下:
1:用户名
2:明文密码
3:用户IP
以上三个都是字符串
4:错误代码(参照getErrorInfo函数,内容有两个字节,第一个字节是pktType,第二个字节是ErrCode
'''
class SimpleMessage():
def __init__(self,data=None):
#print data
if data:
data=bytearray(data)
self.pktType=data[2]
self.attNum=data[3]
self.attList=decodeTLV(self.attNum,data[4:])
else:
self.pktType=3
self.attNum=0
self.attList=[]
def createPacket(self):
header=bytearray([88,99,self.pktType,self.attNum])
tlvBuf=encodeTLV(self.attList)
#print "simple packet:"
#printBin(header+tlvBuf)
return header+tlvBuf
def createRequest(self,userName,userPass,userIP):
self.pktType=1
self.addAttr(1, userName)
self.addAttr(2, userPass)
self.addAttr(3, userIP)
def createSuccess(self):
self.pktType=3
def createError(self,mytype,myerrCode):
self.pktType=4
data=bytearray(2)
data[0]=mytype
data[1]=myerrCode
self.addAttr(4, data)
def getAttr(self,attType):
for myattType,attLen,attData in self.attList:
if attType==myattType:
return attData
return None
def addAttr(self,mytype,data):
#增加属性,并对属性数量加1
self.attList.append((mytype,len(data),data))
self.attNum=self.attNum+1
#获取当前报文的错误信息
def getError(self):
#print "getError"
mydata=self.getAttr(4)
if mydata is None:
return "no error"
else:
mytype=mydata[0]
myerror=mydata[1]
#print mytype,myerror
return getErrorInfo(mytype,myerror)
#处理portal协议报文的类
class PortalMessage:
def __init__(self,secret,data=None):
self.attrList=[]
self.secret=secret
if data :
self.decodePkt(data)
else:
self.Version=2
self.type=0
self.papChap =0
self.rsvd =0
self.SerialNo=0
self.ReqIdentifier=0
self.userIp=0
self.userPort =0
self.errCode=0
self.attrNum=0
self.attrList=[]
self.Authenticator=self.initAuthenticator()
#生成随机序列号
def getSerialNo(self):
return random.randint(1000,65000)
#初始化Authenticator为16个0
def initAuthenticator(self):
return bytearray(16)
#根据ErrCode及报文类型返回具体的错误信息
def getErrInfo(self,ErrCode):
return getErrorInfo(self.type,ErrCode)
#通过challenge计算chap_password
def createChapPassword(self,password,challenge,reqID):
#取reqID的低8位
myreqID=reqID & 0b0000000011111111
mydata=bytearray()
mydata.append(myreqID)
mydata=mydata+password+challenge
#print "chap buff data:"
#printBin(mydata)
chap_pass=md5_constructor(mydata).digest()
#print "chap:"
#printBin(chap_pass)
return chap_pass
#创建字节流,用于在网络上发送
def createPacket(self):
buf=bytearray()
buf.append(self.Version)
buf.append(self.type)
buf.append(self.papChap)
buf.append(self.rsvd)
#序列号,注意要用大端模式
tb=struct.pack('!H',self.SerialNo)
buf=buf+tb
#reqID,注意要用大端模式
tb=struct.pack('!H',self.ReqIdentifier)
buf=buf+tb
#用户IP,注意要用大端模式
tb=struct.pack('!L',self.userIp)
buf=buf+tb
#用户端口,注意要用大端模式
tb=struct.pack('!H',self.userPort)
buf=buf+tb
buf.append(self.errCode)
buf.append(self.attrNum)
buf=buf+self.Authenticator
#属性
for AttrType,AttrLen,AttrStr in self.attrList:
buf.append(AttrType)
buf.append(AttrLen+2)
buf=buf+AttrStr
#print "Packet Data buffer"
#printBin(buf)
return buf
#计算Authenticator
def createAuthenticator(self):
buf=self.createPacket()
buf=buf+self.secret
self.Authenticator=md5_constructor(buf).digest()
#print "authenticator:"
#printBin(self.Authenticator)
return self.Authenticator
#创建一个新的消息报文
def createNewMsg(self,msgType,userIP,sn,reqID):
self.type=msgType
self.userIp=userIP
self.SerialNo=sn
self.ReqIdentifier=reqID
#创建挑战报文
def createChallenge(self,userIP):
sn=self.getSerialNo()
#sn=4
self.createNewMsg(REQ_CHALLENGE,userIP,sn,0)
#计算Authenticator
self.createAuthenticator()
#创建请求认证报文
def createAuth(self,userIP,challengeID,sn,reqID,userName,userPass):
#初始化AUTH请求报文
self.createNewMsg(REQ_AUTH,userIP,sn,reqID)
#先计算chap密码
chap_pass=self.createChapPassword(userPass,challengeID,reqID)
#增加chap密码属性
self.addAttr(ATTR_ChapPassWord, chap_pass)
#增加用户名属性
self.addAttr(ATTR_UserNAME, userName)
#计算Authenticator
self.createAuthenticator()
#创建BAS认证成功后的回复报文AFF_ACK_AUTH
def createAFF(self,userIP,sn,reqID):
#初始化报文
self.createNewMsg(AFF_ACK_AUTH,userIP,sn,reqID)
self.createAuthenticator()
#创建请求下线报文,注意文档上要求提供上线时的reqID,但实际上好像用0即可
def createLogout(self,userIP):
#初始化报文
sn=self.getSerialNo()
self.createNewMsg(REQ_LOGOUT,userIP,sn,0)
self.createAuthenticator()
#增加属性
def addAttr(self,mytype,data):
#增加属性,并对属性数量加1
self.attrList.append((mytype,len(data),data))
self.attrNum=self.attrNum+1
#获取属性
def getAttr(self,mytype):
for attrType,attrLen,attrData in self.attrList:
if attrType==mytype:
return attrData
return None
#解码报文
def decodePkt(self,data):
#data=bytearray([2,2,0,0,0,4,0,2,0xac,0x4b,0x64,0xfd,0,0,0,1,0x4e,0x1f,0xf4,0xeb,0x21,0x57,0x50,0xbc,0x1d,0x4a,0xa4,0xe4,0x8b,0x25,0x76,0x11,3,0x12,0xbb,0x0b,0xcd,0x57,0x41,0x5d,0x3d,0xb7,0xb7,0xcd,0x5b,0x39,0x3f,0xc1,0x29,0xe3])
self.Version,self.type,self.papChap,self.rsvd,self.SerialNo,\
self.ReqIdentifier,self.userIp,self.userPort,self.errCode,self.attrNum \
=struct.unpack('!BBBBHHLHBB',data[0:16])
self.Authenticator=data[16:32]
self.decodeAttr(data[32:])
#self.userIp=
#print self.Authenticator
#print self.attrList
#printBin(self.attrList[0][2])
def decodeAttr(self,data):
self.attrList=decodeTLV(self.attrNum,data)
#print self.attrList
#return
#while(1):
# if len(data)==0:
# break
# mytype=data[0]
# mylen=data[1]-2
# myvalue=data[2:data[1]]
# self.attrList.append((mytype,mylen,myvalue))
# data=data[data[1]:]
def test():
#log.startLogging(sys.stdout, 0)
secret='1234567890123456'
userIP='172.75.100.253'
userName='web@default0'
userPass='123456'
myip=IPy.IP(userIP)
userIP=myip.int()
#第一步,发送请求挑战报文
print u"第一步,发送请求挑战报文"
msg=PortalMessage(secret)
msg.createChallenge(userIP)
msg.createPacket()
#data= msg.createAuthenticator()
#printBin(data)
#第二步解码BAS发送回来的挑战响应报文,并获取一些关键字段的值,data是我模拟的BAS发过来的报文
print u"第二步解码BAS发送回来的挑战响应报文,并获取一些关键字段的值"
data=bytearray([2,2,0,0,0,4,0,2,0xac,0x4b,0x64,0xfd,0,0,0,1,0x4e,0x1f,0xf4,0xeb,0x21,0x57,0x50,0xbc,0x1d,0x4a,0xa4,0xe4,0x8b,0x25,0x76,0x11,3,0x12,0xbb,0x0b,0xcd,0x57,0x41,0x5d,0x3d,0xb7,0xb7,0xcd,0x5b,0x39,0x3f,0xc1,0x29,0xe3])
msg=PortalMessage(secret,data)
challengeID=msg.getAttr(ATTR_Challenge)
reqID=msg.ReqIdentifier
sn=msg.SerialNo
#第三步发送认证请求报文
print u'第三步发送认证请求报文'
msg_auth=PortalMessage(secret)
msg_auth.createAuth(userIP,challengeID,sn,reqID,userName,userPass)
msg_auth.createPacket()
#第四步解码BAS发送的认证响应报文
print u'第四步解码BAS发送的认证响应报文'
data=bytearray([2,4,0,0,0,4,0,2,0xac,0x4b,0x64,0xfd,0,0,0,0,0xa2,0xf6,0x12,8,0x97,0xfc,0x14,0x3d,0x29,0xac,0xcf,0xb5,0x58,0x2e,0x8b,0x89])
msg_authact=PortalMessage(secret,data)
print "ErrCode:"
print msg_authact.errCode
reqID= msg_authact.ReqIdentifier
sn= msg_authact.SerialNo
#第五步发送响应认证成功报文AFF_ACK_AUTH
print u'第五步发送响应认证成功报文AFF_ACK_AUTH'
msg_aff=PortalMessage(secret)
msg_aff.createAFF(userIP, sn, reqID)
msg_aff.createPacket()
#第六步发送下线报文
print u'第六步发送下线报文'
msg_logout_req=PortalMessage(secret)
msg_logout_req.createLogout(userIP)
msg_logout_req.createPacket()
if __name__ == '__main__':
test()
主要是PortalMessage这个类,在test函数里面模拟了数据的收发过程createPacket()这个函数返回二进制数据流,可以直接在socket函数里面发送。data是模拟收到的BAS发过来的报文,现实中应该是从socket里recv到的数据。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)