OpeNHP代码解读
1. 层级架构
- 上层逻辑组件层负责UDP的连接建立、维护与断开
- Device层负责:1.将上层的消息明文转为NHP报文并发送到连接;2.将从连接收到的NHP报文转化为消息明文并提供上层处理
- 上层逻辑组件提供
2. 连接管理
- 上层逻辑组件可以建立并维护多个连接UdpConn,根据实际需求创建所需对象成员。每一个UdpConn起一个线程进行收发包操作。
- 每一个UdpConn需要建立一个Device层的ConnData,并向Device ConnData传递实际连接中的远端地址,报文收发通道,cookie等。
- 每一个UdpConn允许进行多次双向的transaction或单向发包。(agent除外,原则上agent每次请求都创建一个新的连接)
- 每一个transaction都建立一个自身的线程和通道用于维持交互操作,超时后自行销毁。Local transaction(本地创建的交互)由device统一管理,Remote transaction(远端创建的交互)由远端连接管理,transaction的回应在收发包时需要找出相应的transaction线程进行后续操作。
3. 对象命名
- 上层逻辑组件在收发方向上可能具有多重身份,Device层中使用initiator和responder表示发起方和接收方。
4. 报文缓冲区的创建与销毁(回收)
- 为了提高吞吐率,报文缓冲区不采用自动垃圾回收机制而采用waitpool分配回收机制。
- 接收:device创建报文缓冲区接收网络数据,根据NHP包头对报文进行解析与校验。解析结果存储在ResponderSessionParams结构中(名称不好理解,可能会改变)。明文消息仍然会使用报文缓冲区。缓冲区的销毁分两种情况,单向通信的结构体在上层应用获取明文消息后销毁。transaction接收缓冲区在transaction结束后销毁。
- 发送:device创建报文缓冲区,填充包头并对消息进行加密后存储在InitiatorSessionParams结构中并发送。transaction发送在未收到对端回应时会重试发送。缓冲区的销毁分两种情况,单向通信的结构体在发送后销毁。transaction发送的缓冲区在transaction结束后销毁。
消息的加密与解密: 连接中接收到的UDP原始数据会被device解析并放入device的MsgToPacketQueue队列中,等待后端处理。 发送消息到连接时,需构建initiatorsessionstarter结构传入消息信息与连接信息,放入device的MsgToPacketQueue队列中,device会将消息进行加密发出。
5. NHP-Device 架构设计
Device负责NHP报文与消息的转换。Device初始化时需要指定类型和私钥。Device视自身类型只对相应的包进行处理。
用于承载发送和接收报文的buffer比较大,所以由Device的内存Pool统一发放并回收(如果依赖于Go后台垃圾回收,高并发时会造成大量内存开销)。所以在开发时一定要注意buffer的分配Device.AllocatePoolPacket() 和回收Device.ReleasePoolPacket()。
- 报文buffer回收点位于
- 发送报文被发送后(本地transaction除外)
- 接收报文解析完毕时(远程transaction除外)
- 本地或远程transaction线程停止时
- 报文buffer回收点位于
上层逻辑调用接口SendMsgToPacket将消息转换成加密报文并发送到连接。
上层逻辑调用接口RecvPacketToMsg将加密报文解析成消息后放入DecryptedMsgQueue队列并等待处理(通常情况)。
- 特殊情况:如果请求发起方已指定接收通道,解析后的消息会被送到请求方指定的消息通道ResponseMsgCh,而不放进常规消息队列进行排队。
交互(transaction):一次请求需要等待一次回复的操作称为交互。一次由Device发起的交互请求为本地交互(LocalTransaction),一次由Device接收到的交互请求为远程交互(RemoteTransaction)。由于回应报文需要继承请求报文生成的ChainKey,所以所有的交互分发由Device进行管理。
连接上下文(ConnectionData):由上层逻辑传入的与连接相关的所有信息,Device在加密消息后将报文发送到连接。一个连接可以进行多个transaction。
在建立发送请求时,需要创建MsgAssembler结构体。
Agent和AC必须填写消息类型HeaderType、对端RemoteAddr、对端公钥PeerPk和消息明文Message(如无特殊情况都采用消息压缩)。将填写好的MsgAssembler发给各自的sendMessageRoutine() 即可进行新连接的建立或寻找已存在连接并进行转换后报文的发送。
Server必须填写消息类型HeaderType、连接上下文ConnData、对端公钥PeerPk和消息明文Message(如无特殊情况都采用消息压缩)。将填写好的MsgAssembler发给Device.SendMsgToPacket() 即可进行转换后报文的发送。
如果存在交互,可以直接使用上一条获得的 *PacketParserData填入MsgAssembler结构体的PrevParserData字段,从而可以省略填写RemoteAddr、ConnData、PeerPk。
如果请求期待回复数据,需要创建一个接收PacketParserData的通道,并对MsgAssembler结构体的ResponseMsgCh字段赋值。
6. NHP-Server
6.1 NHP-Server 架构设计
- Server启动时监听特定端口,等待Agent和AC进行连接。并由Agent或AC主动触发向Server的通信。不存在Server向Agent或AC主动建立连接的情况,通常情况下这种连接会跨防火墙或NAT导致不能建立。
- 特殊情况:Server在收到Agent发起的敲门处理时,鉴权后需要主动向AC发起开门请求,并等待回应。
发送消息时,向sendMsgCh发送创建好的MsgAssembler(必须从已有连接中指定ConnData)。MsgAssembler经过加密后会从此连接发出
- 接收到报文时,会将报文进行解密获取明文消息。由msghandler分别进行处理。
6.2 NHP-Server 配置文件
etc/config.json
{
// (mandatory) private key in base64 format
"privateKey": "eHdyRHKJy/YZJsResCt5XTAZgtcwvLpSXAiZ8DBc0V4=",
// (mandatory) specify the udp listening port
"listenPort": 62206,
// whether to validate peer's public key when receiving NHP packet from agent. If true, server must have a pre-recorded public key pool (in "agents" field) of all allowed agents. If false, server skip public key validation, so it reduces secure level.
"disableAgentValidation": false,
// list of preset allowed AC peers. only public key and expire time are needed. It has the same effect as AddACPeer()
"acs": [
{
// type: NHP-AC
"type": 3,
// public key in base64 format
"pubKeyBase64": "Fr5jzZDVpNh5m9AcBDMtHGmbCAczHyPegT8IxQ3XAzE=",
// expire time for the public key (seconds from epoch)
"expireTime": 1716345064
}
],
// list of preset allowed agent peers. only public key and expire time are needed. It has the same effect as AddAgentPeer()
"agents": [
{
// type: NHP-Agent
"type": 1,
// public key in base64 format
"pubKeyBase64": "WnJAolo88/q0x2VdLQYdmZNtKjwG2ocBd1Ozj41AKlo=",
// expire time for the public key (seconds from epoch)
"expireTime": 1716345064
}
],
// (optional) placeholder of preset url for possible authorization service provider
"asps": {
"abc.com": {
"aspId": "abc.com",
"urlAddr": "http://120.92.16.228:30088",
"urlOTP": "/nhp/api/v1/preAuth",
"urlReg": "/nhp/api/v1/registerAgent",
"urlAuth": "/nhp/api/v1/verifyAuth",
"urlList": "/nhp/api/v1/resourceList"
}
},
// (optional) specify other source IP addresses to be opened by the ac that may come along with certain agent IP address
"srcAsscAddrs": {
"192.168.2.27": [
{
"ip": "192.168.2.26",
"port": 54222
},
{
"ip": "192.168.2.28",
"port": 54223
}
]
},
// preset resources for udp knocking
"udpRess": {
// ID of authorization service provider
"abc_group": {
// ID of resource group
"app_resource_group_000": {
// skip service provider authorization and use this preset resource group
"skipAuth": true,
// set the desired open time for this resource group (in second)
"opnTime": 120,
"resInfo": {
// name of resource
"apiServer": {
// (optional) hostname overrides addr.ip at knock feedback
"host": "api.abc.com",
// (mandatory) request ac to open which layer 4 address and protocol of this resource
"addr": {
// (mandatory) request ac to open traffic destinated to the public IP address of this resource
"ip": "12.34.56.78",
// (optional) request ac to open traffic destinated to the port number where this resource hosts on. empty or 0 means open all port numbers.
"port": 443,
// (optional) protocol, "tcp": request ac to open only tcp traffic, "udp": request ac to open only udp traffic, empty: request ac to open tcp + udp + icmp echo traffic
"proto": "tcp"
},
}
}
}
}
},
// preset resources for http knocking
"httpRess": {
// ID of authorization service provider
"abc_group": {
// ID of the resource group, usually it means AppId
"app_resource_group_001": {
// set the desired open time for this resource group (in second)
"opnTime": 120,
// contains multiple resources
"resInfo": {
// name of resource
"apiServer": {
// (optional) hostname overrides addr.ip at knock feedback
"host": "api.abc.com",
// (mandatory) request ac to open which layer 4 address and protocol of this resource
"addr": {
// (mandatory) request ac to open traffic destinated to the public IP address of this resource
"ip": "12.34.56.78",
// (optional) request ac to open traffic destinated to the port number where this resource hosts on. empty or 0 means open all port numbers.
"port": 443,
// (optional) protocol, "tcp": request ac to open only tcp traffic, "udp": request ac to open only udp traffic, empty: request ac to open tcp + udp + icmp echo traffic
"proto": "tcp"
},
// (optional) the private layer 4 address of the ac. In some network, server may communicate with ac using private addresses.
"acAddr": {
"ip": "172.16.1.2",
"port": 443
},
// whether to append ":port" at the end of hostname/ip at knock feedback. For example, set this field to false if this resource use https and requesting ac to open port 443.
"portSuffix": false
},
// another resource
"webServer": {
"host": "www.abc.com",
"addr": {
"ip": "23.45.67.89",
"port": 8080,
"proto": ""
},
"portSuffix": true
}
},
// (optional) additional key info for server calling further authroization APIs
"accessKey": "b3458c581ef0efb7b669",
"secretKey": "f21c2a02c09a641a11cf"
}
},
// another authorization service provider
"xyz_org": {
"abcd1234": {
"opnTime": 120,
"resInfo": {
"udpServer": {
"host": "server.xyz.net",
"addr": {
"ip": "1.2.3.4",
"port": 443,
"proto": "udp"
},
"portSuffix": false
}
},
// (optional) additional key info for server calling further authroization APIs
"appKey": "demo-l2T0J3U3mQZ3",
"appSecret": "hVqd8eOqCFg5cc1D2ouACs3q"
}
}
}
}
7. NHP-AC
7.1 NHP-AC 架构设计
AC支持与多台Server互相进行通信。所有连接均为AC主动向Server发起。AC通过发送心跳包和NHP-AOL包维持与Server的连接。
AC与Server通信失效后,将尝试重新建立连接,如果一直无法与任何一台Server建立连接,则进入失效状态。
AC在启动后即开始与预设的服务器周期性建立连接并保持连接(AC很有可能在内网,所以不能由服务器先发连接)。连接时发送NHP_DOL消息,在收到服务器的回应后确认连接。连接期间视情况进行发送NHP_KPL消息保持连接。由maintainServerConnectionRoutine实现。
AC处理服务器发送过来的NHP_DOP消息,判断请求方的serviceId, appId是否匹配并进行IPSET操作,完成后返回NHP_DRT消息。
发送消息时,向sendMsgCh发送创建好的MsgAssembler(必须指定RemoteAddr)。如果连接没有建立,AC会尝试建立并记录该连接。同时对此连接开启接收线程。MsgAssembler经过加密后会从此连接发出
接收到报文时,会将报文进行解密获取明文消息。由msghandler分别进行处理。
7.2 NHP-AC IP放行模式
IP放行模式分为两种:
ipPassMode为0(默认)时为立即放行模式,门禁开门时将以敲门来源IP地址为准。
ipPassMode为1时为预访问模式,门禁开门前将先开启对应协议的临时端口并返回server临时端口和临时访问token,在短时间内需由agent携带临时访问token进行临时连接,如果临时连接有效,则开门时放行将以此次临时连接的来源IP为准。
7.3 NHP-AC 配置文件
etc/config.toml
[AC]
# (optional) assign an unique id for this ac
ACId = "abc_group_ac_001"
# (mandatory) specify the private key in base64 format
ACPrivateKey = "+B0RLGbe+nknJBZ0Fjt7kCBWfSTUttbUqkGteLfIp30="
# 0: default, passing the knock source IP
# 1: use pre-access procedure to determine the passing source IP
IpPassMode = 0
# (optional) ID of authorization service provider this ac belongs to
AuthServiceId = "abc_group"
# (optional) ID of resources controlled by this ac
ResourceIds = ["abc_group_web_server", "abc_group_api_server"]
# (optional) ID of organization
OrganizationId = "5f3e36149fa95c0414408ad4"
# server peers list
[[Servers]]
# (optional) the server's hostname. Its resolved address overrides the "Ip" field
Host = ""
# IP address of the server peer
Ip = "192.168.80.35"
# listening port for the server peer
Port = 62206
# type: NHP-Server
Type = 2
# specify the server peer's public key in base64 format
PublicKey = "WqJxe+Z4+wLen3VRgZx6YnbjvJFmptz99zkONCt/7gc="
# expire timestamp of the public key (seconds from epoch)
ExpireTime = 1716345064
# another server
#[[Servers]]
# Ip = "192.168.135.1"
# Port = 7776
# Type = 2
# PublicKey = "dstv1KlD2oVXiwgOxWtgZd+YmrOhU46W3emTGrHRADk="
# ExpireTime = 1716345064
8. NHP-Agent
8.1 NHP-Agent 架构设计
Agent只与Server之间进行通信。Agent主动向Server发起短连接。不存在Agent在未建立连接时被动接收Server消息的情况。
发送消息时,向sendMsgCh发送创建好的MsgAssembler(必须指定RemoteAddr)。如果连接没有建立,agent会尝试建立并记录该连接。同时对此连接开启接收线程。MsgAssembler经过加密后会从此连接发出
接收到报文时,会将报文进行解密获取明文消息。由msghandler分别进行处理。
8.2 NHP-Agent 配置文件
etc/config.json
{
// (mandatory) specify the private key in base64 format
"privateKey": "+Jnee2lP6Kn47qzSaqwSmWxORsBkkCV6YHsRqXCegVo=",
// (optional) ID of authorization service provider this agent belongs to
"aspId": "abc_group",
// (mandatory) an user object is necessary to carry out knock requests
"user": {
"userId": "zengl",
"devId": "0123456789abcdef",
"orgId": "abc.com.cn"
},
// preset resources to begin knock after start
"knockRess": [
{
"aspId": "abc_group",
"resId": "app_resource_group_001",
"serverKey": "WqJxe+Z4+wLen3VRgZx6YnbjvJFmptz99zkONCt/7gc="
}
],
// list of preset allowed server peers to send knock request. It has the same effect as AddServer()
"servers": [
{
// (optional) the server's hostname. Its resolved address overrides the "Ip" field
"host": "",
// IP address of the server peer
"ip": "192.168.80.35",
// listening port for the server peer
"port": 62206,
// type: NHP-Server
"type": 2,
// public key in base64 format
"pubKeyBase64": "WqJxe+Z4+wLen3VRgZx6YnbjvJFmptz99zkONCt/7gc=",
/// expire time for the public key (seconds from epoch)
"expireTime": 1716345064
}
]
}
9. Log 设计
日志log设计为异步写入,相比同步日志写入,在调用时不会立即进行写入日志文件的I/O操作而影响正常业务逻辑,在高并发时可以聚合多条日志并合并为一次文件写入,大幅减少文件I/O操作次数。
Logger对象可以单独创建使用(NewLogger()),也可以在应用程序启动时指定Package全局变量glbLogger,供整个工程使用。
注意:应用程序结束前,需调用Logger.Close(),确保最后缓存的日志能够写入文件。