go-ethereum中p2p-rlpx源码学习
rlpx是一种传输协议,它是基于tcp的,用于节点之间的信息,rlpx并不是什么的缩写,而是基于rlp序列化命名的。代码主要集中在p2p包的rlpx.go中,以太坊主要借助这个协议进行节点间数据传输,所传输的数据也是加密的
ECIES加密
ECIES全称Elliptic Curve Integrated Encryption Scheme,即椭圆曲线集成加密方案,是一种混合加密方法。
假设Alice要发发送一条加密消息给Bob,Alice知晓Bob的公钥KB(后文中||符号表示简单的拼接,如a||b=ab)。为了加密消息m,Alice进行如下流程:
- 首先生成一个随机数r,然后生成公钥R=r*G。
- 计算出共享秘密S = Px。其中(Px,Py)= r*KB。
- 得到S后,计算kE || kM = KDF(S,32)(KDF是密钥生成函数,见文档5.8节)。
- 随机生成一个初始化向量iv。
- 最后发送给Bob的消息为:R || iv || c || d,其中c为以kE为密钥,以iv为初始化向量对m进行AES的CTR模式加密(CTR模式,AES加密);d为以kM为密钥,使用SHA-256作为摘要函数,对iv||c计算HMAC(HMAC)
Bob在收到消息后,进行如下步骤:
- 计算共享秘密S,S=Px,其中(Px,Py) = kB*R(注意kB是Bob私钥,KB是Bob公钥,利用的椭圆曲线加密原理,简单证明如下:kB*R = kB*r*G=r*KB =(Px,Py))
- 根据共享秘密S计算出密钥:kE || kM = KDF(S,32)
- 先验证d,即对iv || c使用kM作为密钥计算HMAC
- 再利用kE作为密钥,以iv为初始化向量,使用AES算法的CTR模式进行解密得到明文m
上述步骤的大致思想就是利用非对称加密算法安全的交换密钥,在使用对称加密算法加密消息,弥补非对称算法的性能缺陷。
握手协议
- 发起者尝试连接接受者,并发送auth消息
- 接受者验证auth消息
- 接受者构造应答auth-ack消息
- 接受者计算共享秘密,发送第一个加密消息:hello
- 发起者收到auth-ack消息并计算共享秘密
- 发起者发送它的第一个加密消息:hello
- 双方验证各自收到的对方第一个加密消息
- 若有效,则加密握手完成
auth消息如下:
auth = auth-size || enc-auth-body
auth-size:enc-auth-body的长度
enc-auth-body = ecies.encrypt(recipient-pubk, auth-body || auth-padding, auth-size) #auth-size不会被写入密文但是会写入HMAC
auth-padding :任意填充数据
auth-body = [sig, initiator-pubk, initiator-nonce, auth-vsn, ...]
auth-vsn = 4
auth-ack消息如下:
ack = ack-size || enc-ack-body
ack-size = enc-ack-body的长度
enc-ack-body = ecies.encrypt(initiator-pubk, ack-body || ack-padding, ack-size)
ack-padding :任意填充数据
ack-body = [recipient-ephemeral-pubk, recipient-nonce, ack-vsn, ...]
ack-vsn = 4
几个秘密生成算法:
- 静态共享秘密:static-shared-secret = ecdh.agree(privkey, remote-pubk)。ecdh是一种基于ecc的密钥协商算法
- 临时密钥: ephemeral-key = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
- 共享秘密:shared-secret = keccak256(ephemeral-key || keccak256(nonce || initiator-nonce))
- aes密钥:aes-secret = keccak256(ephemeral-key || shared-secret)
- mac密钥:mac-secret = keccak256(ephemeral-key || aes-secret)
帧
握手成功之后所有消息都是以帧的形式传播。帧的目的是在一个连接上复用多个功能。其次,由于框架型的消息为消息身份验证提供了合理的分界点,因此加密和身份验证流变得非常简单。帧通过在握手过程中生成的密钥进行加密和身份验证。
帧格式如下:
frame = header-ciphertext || header-mac || frame-ciphertext || frame-mac
header-ciphertext = aes(aes-secret, header)
header = frame-size || header-data || header-padding
header-data = [capability-id, context-id]
capability-id = integer, always zero
context-id = integer, always zero
header-padding = zero-fill header to 16-byte boundary
frame-ciphertext = aes(aes-secret, frame-data || frame-padding)
frame-padding = zero-fill frame-data to 16-byte boundary
MAC
就是所谓的消息认证码,rlpx中使用keccak256作为hash函数,并且使用了两个不同的mac用作不同方向的通信,分别是egress-mac和ingress-mac。这两个mac会在通信中不断更新,其构造如下
发送方初始状态:
egress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)
ingress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)
接收方初始状态:
egress-mac = keccak256.init((mac-secret ^ initiator-nonce) || ack)
ingress-mac = keccak256.init((mac-secret ^ recipient-nonce) || auth)
当帧发送时,要根据发送的数据更新egress-mac:
header-mac-seed = aes(mac-secret, keccak256.digest(egress-mac)[:16] ^ header-ciphertext)
egress-mac = keccak256.update(egress-mac, header-mac-seed)
header-mac = keccak256.digest(egress-mac)[:16]
帧mac的计算如下:
egress-mac = keccak256.update(egress-mac, frame-ciphertext)
frame-mac-seed = aes(mac-secret, keccak256.digest(egress-mac)[:16]) ^ keccak256.digest(egress-mac)[:16]
egress-mac = keccak256.update(egress-mac, frame-mac-seed)
frame-mac = keccak256.digest(egress-mac)[:16]
接收方收到后,以同样方式更新ingress-mac,通过对比来判断通信完整性。
源码
首先回顾一下rlpx的服务中的创建:
newTransport func(net.Conn) transport
srv.newTransport = newRLPX
func newRLPX(fd net.Conn) transport {
fd.SetDeadline(time.Now().Add(handshakeTimeout))
return &rlpx{fd: fd}
}
type rlpx struct {
fd net.Conn
rmu, wmu sync.Mutex
rw *rlpxFrameRW
}
rlpx之所以能被赋值给newTransport是因为他实现了transport接口:
type transport interface {
doEncHandshake(prv *ecdsa.PrivateKey, dialDest *ecdsa.PublicKey) (*ecdsa.PublicKey, error)
doProtoHandshake(our *protoHandshake) (*protoHandshake, error)
MsgReadWriter
close(err error)
}
type MsgReadWriter interface {
MsgReader
MsgWriter
}
type MsgReader interface {
ReadMsg() (Msg, error)
}
type MsgWriter interface {
WriteMsg(Msg) error
}
doEncHandshake
在服务的启动中配置tcp连接时,每接到一个tcp连接,在setupConn中调用了doEncHandshake执行加密握手:
func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *ecdsa.PublicKey) (*ecdsa.PublicKey, error) {
var (
sec secrets
err error
)
if dial == nil {
sec, err = receiverEncHandshake(t.fd, prv)
} else {
sec, err = initiatorEncHandshake(t.fd, prv, dial)
}
if err != nil {
return nil, err
}
t.wmu.Lock()
t.rw = newRLPXFrameRW(t.fd, sec)
t.wmu.Unlock()
return sec.Remote.ExportECDSA(), nil
}
当收到一个连接时,dial为null,调用receiverEncHandshake,当发起一个连接时调用initiatorEncHandshake。先看发起一个连接:
func initiatorEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, remote *ecdsa.PublicKey) (s secrets, err error) {
h := &encHandshake{initiator: true, remote: ecies.ImportECDSAPublic(remote)}
authMsg, err := h.makeAuthMsg(prv)
if err != nil {
return s, err
}
authPacket, err := sealEIP8(authMsg, h)
if err != nil {
return s, err
}
if _, err = conn.Write(authPacket); err != nil {
return s, err
}
authRespMsg := new(authRespV4)
authRespPacket, err := readHandshakeMsg(authRespMsg, encAuthRespLen, prv, conn)
if err != nil {
return s, err
}
if err := h.handleAuthResp(authRespMsg); err != nil {
return s, err
}
return h.secrets(authPacket, authRespPacket)
}
首先初始化encHandshake,并用其构造一个auth消息:
func (h *encHandshake) makeAuthMsg(prv *ecdsa.PrivateKey) (*authMsgV4, error) {
h.initNonce = make([]byte, shaLen)
_, err := rand.Read(h.initNonce)
if err != nil {
return nil, err
}
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
if err != nil {
return nil, err
}
token, err := h.staticSharedSecret(prv)
if err != nil {
return nil, err
}
signed := xor(token, h.initNonce)
signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
if err != nil {
return nil, err
}
msg := new(authMsgV4)
copy(msg.Signature[:], signature)
copy(msg.InitiatorPubkey[:], crypto.FromECDSAPub(&prv.PublicKey)[1:])
copy(msg.Nonce[:], h.initNonce)
msg.Version = 4
return msg, nil
}
首先随机一个随机数,长度32字节。然后构造一对秘钥用于ECDH,然后计算静态共享秘钥(使用自己私钥和对方公钥,按照ECDH算法)。接着将共享秘钥和自己的随机数异或,并将结果用ecc算法签名。字后构造出auth消息,包含签名值,自己的公钥,自己的随机数,以及版本号。
生成auth消息后,按EIP8协议进行封装
func sealEIP8(msg interface{}, h *encHandshake) ([]byte, error) {
buf := new(bytes.Buffer)
if err := rlp.Encode(buf, msg); err != nil {
return nil, err
}
pad := padSpace[:mrand.Intn(len(padSpace)-100)+100]
buf.Write(pad)
prefix := make([]byte, 2)
binary.BigEndian.PutUint16(prefix, uint16(buf.Len()+eciesOverhead))
enc, err := ecies.Encrypt(rand.Reader, h.remote, buf.Bytes(), nil, prefix)
return append(prefix, enc...), err
}
可见先进行rlp编码,然后进行填充随机长度的内容(最少100字节),然后计算长度前缀,前缀表示长度,包含填充后内容长度以及加上公钥初始化向量及MAC消息后的总长度。最后对内容进行加密(加密流程见前文ECIES加密),最后在头部补上前缀得到最后封装数据。
继续回到initiatorEncHandshake一切消息准备完毕后,通过连接发送握手包,下面我们分析收到一个握手请求时的逻辑。
同样还是doEncHandshake,收到一个连接请求时,也就是加密握手包,此时dial为空,执行receiverEncHandshake:
func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey) (s secrets, err error) {
authMsg := new(authMsgV4)
authPacket, err := readHandshakeMsg(authMsg, encAuthMsgLen, prv, conn)
if err != nil {
return s, err
}
h := new(encHandshake)
if err := h.handleAuthMsg(authMsg, prv); err != nil {
return s, err
}
authRespMsg, err := h.makeAuthResp()
if err != nil {
return s, err
}
var authRespPacket []byte
if authMsg.gotPlain {
authRespPacket, err = authRespMsg.sealPlain(h)
} else {
authRespPacket, err = sealEIP8(authRespMsg, h)
}
if err != nil {
return s, err
}
if _, err = conn.Write(authRespPacket); err != nil {
return s, err
}
return h.secrets(authPacket, authRespPacket)
}
这个方法表示接受到一个加密握手,首先调用readHandshakeMsg读取信息:这里会尝试用两种标准进行解码,一种是我们前面讲的对握手包进行了加密,这里直接尝试解密,否则进行其他尝试,具体就不细讲了,最后返回所读到的信息,同时解码authMsg。
然后创建了一个encHandshake对象,执行handleAuthMsg方法:
func (h *encHandshake) handleAuthMsg(msg *authMsgV4, prv *ecdsa.PrivateKey) error {
rpub, err := importPublicKey(msg.InitiatorPubkey[:])
if err != nil {
return err
}
h.initNonce = msg.Nonce[:]
h.remote = rpub
if h.randomPrivKey == nil {
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
if err != nil {
return err
}
}
token, err := h.staticSharedSecret(prv)
if err != nil {
return err
}
signedMsg := xor(token, h.initNonce)
remoteRandomPub, err := secp256k1.RecoverPubkey(signedMsg, msg.Signature[:])
if err != nil {
return err
}
h.remoteRandomPub, _ = importPublicKey(remoteRandomPub)
return nil
}
这个方法是用来处理发起方发送的auth消息。获取读取了发起方的公钥、随机数,同时自己也生成了随机的密钥对用于ECDH,之后利用自己密钥和远端公钥生成静态共享秘密,并利用该结果与对方的随机数异或,然后验证签名并得到远端随机公钥
处理完远端的消息收,开始构造响应:
func (h *encHandshake) makeAuthResp() (msg *authRespV4, err error) {
h.respNonce = make([]byte, shaLen)
if _, err = rand.Read(h.respNonce); err != nil {
return nil, err
}
msg = new(authRespV4)
copy(msg.Nonce[:], h.respNonce)
copy(msg.RandomPubkey[:], exportPubkey(&h.randomPrivKey.PublicKey))
msg.Version = 4
return msg, nil
}
响应主要包含自己的随机数,自己的随机公钥和版本号。然后对消息进行封装,根据最新版本调用的是sealEIP8方法,前文已进行过分析。封装后发送给发起人。此时对于接收方握手已经完成,可以构造秘密:
func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) {
ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen)
if err != nil {
return secrets{}, err
}
sharedSecret := crypto.Keccak256(ecdheSecret, crypto.Keccak256(h.respNonce, h.initNonce))
aesSecret := crypto.Keccak256(ecdheSecret, sharedSecret)
s := secrets{
Remote: h.remote,
AES: aesSecret,
MAC: crypto.Keccak256(ecdheSecret, aesSecret),
}
mac1 := sha3.NewLegacyKeccak256()
mac1.Write(xor(s.MAC, h.respNonce))
mac1.Write(auth)
mac2 := sha3.NewLegacyKeccak256()
mac2.Write(xor(s.MAC, h.initNonce))
mac2.Write(authResp)
if h.initiator {
s.EgressMAC, s.IngressMAC = mac1, mac2
} else {
s.EgressMAC, s.IngressMAC = mac2, mac1
}
return s, nil
}
首先利用自己的随机私钥和远端的随机公钥生成共享秘密。然后利用该秘密和双方的随机数生成最终共享秘密。利用共享秘密和最终共享秘密生成aes秘密,再利用aes秘密个共享秘密生成mac秘密,最后远端公钥和aes、mac秘密构成一个主秘密。最后如前文介绍额方法配置EgressMAC和IngressMAC
在接收方发送完响应后,再回到发起人的角度,在initiatorEncHandshake方法中
readHandshakeMsg方法读到一个响应,这个逻辑不在重复,之后处理这个响应:
func (h *encHandshake) handleAuthResp(msg *authRespV4) (err error) {
h.respNonce = msg.Nonce[:]
h.remoteRandomPub, err = importPublicKey(msg.RandomPubkey[:])
return err
}
很简单就是获取对方的随机数和随机公钥,到这里加密握手,实际上是密钥交换的所有信息都交换完毕,双方知晓对方的随机数和随机公钥,可以独立的构建主秘密,所以在initiatorEncHandshake最后发起人也调用secrets构建了主秘密。
最后回到doEncHandshake,在生成主秘密后,构建了rlpx帧的读写器,主要是构造了用于aes和hamc的加密器(都是ctr模式,初始化向量全为0),最后构造了rlpxFrameRW对象。最后将远端公钥返回。
前面握手协议已经完成,中间涉及了许多秘密和密钥,我们这里来梳理一下:
- 静态共享秘密:利用的是自己的私钥和对方的公钥生成,二者按照ECDH算法计算后结果一致,由于用的是自己固定的密钥对,无论在何时这个秘密都是固定的,所以称为静态共享秘密,它的作用是与随机数进行异或后进行签名,进行身份认证,因为只有是公钥对应的私钥的持有人才能计算出和发起方一致的秘密。
- 随机密钥:在握手时双方都会生成一对随机密钥,用于后续通信的加密。
- 共享秘密:和静态共享秘密类似,只不过是用双方随机生成的密钥对按照ECDH算法生成,这样保证了即使密钥泄漏,也只影响本次通信。
- 最终共享秘密:为了保证密钥的随机性,现将二者随机数拼接后进行摘要,然后将结果与共享秘密拼接后再进行摘要得到最终秘密。
- 在发起人对消息加密时,按照的就是前文ecies的流程,通过对方公钥与自己一个随机数生成秘密S,再利用KDF函数生成AES和HMAC要用的密钥。当然为了保护自己的随机数,发起方随机生成了一对密钥,并将公钥发送,用于对方计算S
总结一下加密握手,实际上就是密钥交换,为了构建通信时的密钥,需要知道对方的随机公钥和随机数。最开始发起人只知道接受者的公钥,所以根据ECIES算法传递了自己的随机公钥和随机数,接收方收到之后自己也生成了随机公钥的随机数,测试接收方以及能构建主秘密,发起人收到响应后,根据接收方的随机公钥的随机数同样也能构建一样的主秘密。
doProtoHandshake
在p2p服务的start逻辑中,紧随加密握手之后进行了协议握手
func (t *rlpx) doProtoHandshake(our *protoHandshake) (their *protoHandshake, err error) {
werr := make(chan error, 1)
go func() { werr <- Send(t.rw, handshakeMsg, our) }()
if their, err = readProtocolHandshake(t.rw, our); err != nil {
<-werr // make sure the write terminates too
return nil, err
}
if err := <-werr; err != nil {
return nil, fmt.Errorf("write error: %v", err)
}
t.rw.snappy = their.Version >= snappyProtocolVersion
return their, nil
}
传入的对象是protoHandshake,在sserver的etupLocalNode方法中构建了该对象,存储了自己的协议版本,服务名,ID和自己所有协议信息。在doProtoHandshake中首先调用了Send方法进行发送
func Send(w MsgWriter, msgcode uint64, data interface{}) error {
size, r, err := rlp.EncodeToReader(data)
if err != nil {
return err
}
return w.WriteMsg(Msg{Code: msgcode, Size: uint32(size), Payload: r})
}
首先用rlp进行编码,然后将消息码(握手消息代码为0),消息长度和消息内容封装,并用MsgWriter的WriteMsg进行发送。MsgWriter是一个接口,在doProtoHandshake实际的执行者是rlpxFrameRW,即在加密握手最后构造的对象,它的WriteMsg实现如下:
func (rw *rlpxFrameRW) WriteMsg(msg Msg) error {
ptype, _ := rlp.EncodeToBytes(msg.Code)
if rw.snappy {
if msg.Size > maxUint24 {
return errPlainMessageTooLarge
}
payload, _ := ioutil.ReadAll(msg.Payload)
payload = snappy.Encode(nil, payload)
msg.Payload = bytes.NewReader(payload)
msg.Size = uint32(len(payload))
}
headbuf := make([]byte, 32)
fsize := uint32(len(ptype)) + msg.Size
if fsize > maxUint24 {
return errors.New("message size overflows uint24")
}
putInt24(fsize, headbuf) // TODO: check overflow
copy(headbuf[3:], zeroHeader)
rw.enc.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now encrypted
copy(headbuf[16:], updateMAC(rw.egressMAC, rw.macCipher, headbuf[:16]))
if _, err := rw.conn.Write(headbuf); err != nil {
return err
}
tee := cipher.StreamWriter{S: rw.enc, W: io.MultiWriter(rw.conn, rw.egressMAC)}
if _, err := tee.Write(ptype); err != nil {
return err
}
if _, err := io.Copy(tee, msg.Payload); err != nil {
return err
}
if padding := fsize % 16; padding > 0 {
if _, err := tee.Write(zero16[:16-padding]); err != nil {
return err
}
}
fmacseed := rw.egressMAC.Sum(nil)
mac := updateMAC(rw.egressMAC, rw.macCipher, fmacseed)
_, err := rw.conn.Write(mac)
return err
}
第一步是对msg的code字段进行rlp编码,之后如果压缩可用的话对消息进行压缩,然后更新message的内容和大小。
接下来是头部的构造。首先计算帧大小,包括刚才编码的code字段长度和msg的大小,不过最大长度不能大于^uint32(0) >> 8 (即2^24 - 1)。接下来用3个字节存储长度(大端模式),之后填充{0xC2, 0x80, 0x80}这3个字节。之后对前16个字节进行加密(注意XORKeyStream就是aes的ctr模式加密算法)
再往下是写入头的消息认证码,调用updateMAC方法
func updateMAC(mac hash.Hash, block cipher.Block, seed []byte) []byte {
aesbuf := make([]byte, aes.BlockSize)
block.Encrypt(aesbuf, mac.Sum(nil))
for i := range aesbuf {
aesbuf[i] ^= seed[i]
}
mac.Write(aesbuf)
return mac.Sum(nil)[:16]
}
updateMAC就和前文mac那一节中介绍的一样。会多次用到,这里需要三个参数,第一个是要更新的hash,第二个是加密方法,第三个是种子。首先对hash中当前数据计算hash值然后进行加密,接下来把加密值与种子逐字节的异或,并将结果写入hash,更新完毕,同时计算并返回更新后的hash值。这这段代码中,要更新的是egressMAC,种子是刚才加密过的头数据。
回到WriteMsg中,将返回的hash值从headbuf的第16字节开始写入。到这里头部构造完成,是根据前文帧的定义包含了头部数据密文及头部数据hash。然后通过conn的write发送出去。conn就是最初构造rlpx传入的网络连接。
接下来开始处理帧主体内容,先是构造了StreamWriter,所有传入的数据都会被加密处理,它的writer是一个MultiWriter,包含了conn和egressMAC两个写对象,表示同一份数据会被两个对象同时写。接下来首先写入ptype也就是经rlp编码过msg.Code,之后写入msg的Payload就是帧内容。最后写入填充内容,保证数据是16的整数倍,填充的内容全为0。之后又进行了一次mac更新,依旧是更新egressMAC。种子是刚才持续写入数据后计算出的hash值。最后利用刚才网络通道将mac值发送出去。
上面几步就完成了前文帧定义中的几部分数据的构造与发送。再回到doProtoHandshake中,由于发送时异步进行的,在发送同时进行了readProtocolHandshake操作
func readProtocolHandshake(rw MsgReader, our *protoHandshake) (*protoHandshake, error) {
msg, err := rw.ReadMsg()
if err != nil {
return nil, err
}
if msg.Size > baseProtocolMaxMsgSize {
return nil, fmt.Errorf("message too big")
}
if msg.Code == discMsg {
var reason [1]DiscReason
rlp.Decode(msg.Payload, &reason)
return nil, reason[0]
}
if msg.Code != handshakeMsg {
return nil, fmt.Errorf("expected handshake, got %x", msg.Code)
}
var hs protoHandshake
if err := msg.Decode(&hs); err != nil {
return nil, err
}
if len(hs.ID) != 64 || !bitutil.TestBytes(hs.ID) {
return nil, DiscInvalidIdentity
}
return &hs, nil
}
这是读取对方的握手包。首先调用ReadMsg读取信息,依旧用的是rlpxFrameRW的ReadMsg方法
func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) {
headbuf := make([]byte, 32)
if _, err := io.ReadFull(rw.conn, headbuf); err != nil {
return msg, err
}
shouldMAC := updateMAC(rw.ingressMAC, rw.macCipher, headbuf[:16])
if !hmac.Equal(shouldMAC, headbuf[16:]) {
return msg, errors.New("bad header MAC")
}
rw.dec.XORKeyStream(headbuf[:16], headbuf[:16]) // first half is now decrypted
fsize := readInt24(headbuf)
var rsize = fsize
if padding := fsize % 16; padding > 0 {
rsize += 16 - padding
}
framebuf := make([]byte, rsize)
if _, err := io.ReadFull(rw.conn, framebuf); err != nil {
return msg, err
}
rw.ingressMAC.Write(framebuf)
fmacseed := rw.ingressMAC.Sum(nil)
if _, err := io.ReadFull(rw.conn, headbuf[:16]); err != nil {
return msg, err
}
shouldMAC = updateMAC(rw.ingressMAC, rw.macCipher, fmacseed)
if !hmac.Equal(shouldMAC, headbuf[:16]) {
return msg, errors.New("bad frame MAC")
}
rw.dec.XORKeyStream(framebuf, framebuf)
content := bytes.NewReader(framebuf[:fsize])
if err := rlp.Decode(content, &msg.Code); err != nil {
return msg, err
}
msg.Size = uint32(content.Len())
msg.Payload = content
if rw.snappy {
payload, err := ioutil.ReadAll(msg.Payload)
if err != nil {
return msg, err
}
size, err := snappy.DecodedLen(payload)
if err != nil {
return msg, err
}
if size > int(maxUint24) {
return msg, errPlainMessageTooLarge
}
payload, err = snappy.Decode(nil, payload)
if err != nil {
return msg, err
}
msg.Size, msg.Payload = uint32(size), bytes.NewReader(payload)
}
return msg, nil
}
第一步先从流中读取32字节,就是头部数据,根据刚才发送数据的分析,包括16字节的加密信息和16字节的mac。读取后根据以前16字节信息为种子更新ingressMAC验证数据时候有误,无误的话对前16字节解密,同时读取前3字节信息,还原出长度信息。由于填充的存在还要计算出发送方填充了多少数据,计算出实际长度。下面就从流中读取帧主体信息。同样也将信息写入ingressMAC,并计算出hash值后作为种子更新一次ingressMAC,与数据流的最后16字节也就是帧mac进行对比看数据是否被篡改。确认无误后解密数据。在去除填充数据后,先解码出code信息,然后填充msg其他字段,另外如果压缩可用的话,还要进行解压操作。最后返回message对象。
读取出正确的msg后,回到readProtocolHandshake中,下面就是根据不同的code执行不同的逻辑。这里进区分了为discMsg,不为handshakeMsg以及其他(就是code是handshakeMsg)的情况。我们先看是handshakeMsg的情况,就是刚才我们分析的发送代码发送的code。第一步当然是解码消息,获得protoHandshake对象
,进行简单的检查后返回。这一步是获得了对象p2p所有协议的信息。
在回到doProtoHandshake中,读取到信息后,如果没有错误,则等待Send的完成,如果发送也没有错误,则配置一下snappy,条件就是对方的版本大于5,如果成立以后的信息都会进行压缩。最后返回对方的握手信息,逻辑又回到p2p的server中。这样协议握手也完成了。
在rlpx中也提供了ReadMsg和WriteMsg方法,不过具体实现也是直接调用rlpxFrameRW的读写,前面已经分析过了这里不再重复。
题图来自unsplash:https://unsplash.com/photos/ZIlG-_lwXbg