IPFS Private Network 探秘
How to enable
首先使用 ipfs-swarm-key-gen 生成 swarm.key
go get github.com/Kubuxu/go-ipfs-swarm-key-gen/ipfs-swarm-key-gen
ipfs-swarm-key-gen > ~/.ipfs/swarm.key
要加入一个私有网络首先要得到这个网络的 swarm.key
并保存到 ~/.ipfs/swarm.key
中,如果自定了 IPFS_PATH
,那么就将它放入自定义的路径下。
当启用私网配置后,将无法再使用默认的 bootnode
了,需要设置为私网的 bootnode
。
为了防止私网节点尝试访问默认的 bootnode
我们先将其清除:
ipfs bootstrap rm --all
然后像这样来添加私网自己的 bootnode
ipfs bootstrap add <multiaddr>
例如:
ipfs bootstrap add /ip4/104.236.76.40/tcp/4001/ipfs/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64
网络中的 bootnode
节点与其他节点没有任何不同,所以制作一个 bootnode
还是非常容易的。
也可以通过设置环境变量 LIBP2P_FORCE_PNET=1
来强行启用私网配置,如果没有正确配置 swarm.key
会导致 daemon 启动失败
- 参考资料 IPFS官方文档
原理分析
ipfs-swarm-key-gen
首先看看 key 是如何生成的
//github.com/Kubuxu/go-ipfs-swarm-key-gen/blob/master/ipfs-swarm-key-gen/main.go
func main() {
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
log.Fatalln("While trying to read random source:", err)
}
fmt.Println("/key/swarm/psk/1.0.0/")
fmt.Println("/base16/")
fmt.Print(hex.EncodeToString(key))
}
是不是太简单了?生成一个 32 位的随机数,用 hex.Encode 成一个 64 位 16进制数,谈下一话题。
How to use swarm.key
首先我们来看一下 libp2p 中的 pnet 接口和实现
- go-libp2p-interface-pnet
// go-libp2p-interface-pnet/interface.go
// Protector interface is a way for private network implementation to be transparent in
// libp2p. It is created by implementation and use by libp2p-conn to secure connections
// so they can be only established with selected number of peers.
type Protector interface {
// Wraps passed connection to protect it
Protect(net.Conn) (net.Conn, error)
// Returns key fingerprint that is safe to expose
Fingerprint() []byte
}
Protector 接口说它是实现私网的一种方式,由 libp2p-conn
调用创建加密连接
- go-libp2p-pnet
// go-libp2p-pnet/protector.go
type protector struct {
psk *[32]byte
fingerprint []byte
}
func (p protector) Protect(in net.Conn) (net.Conn, error) {
return newPSKConn(p.psk, in)
}
func (p protector) Fingerprint() []byte {
return p.fingerprint
}
这个 protector
类实现了 Protector
接口,在创建这个类时会去反序列化 key
得到 psk
,那个就不重要了,比较重要的是 newPSKConn
这个包装
func newPSKConn(psk *[32]byte, insecure net.Conn) (net.Conn, error) {
if insecure == nil {
return nil, errInsecureNil
}
if psk == nil {
return nil, errPSKNil
}
return &pskConn{
Conn: insecure,
psk: psk,
}, nil
}
上面的 newPSKConn
返回了 pskConn
,这个类继承了 net.Conn
并且对 read
和 write
进行了有关加密解密的封装
type pskConn struct {
net.Conn
psk *[32]byte
writeS20 cipher.Stream
readS20 cipher.Stream
}
func (c *pskConn) Read(out []byte) (int, error) {
if c.readS20 == nil {
// 如果是第一个包,那么前 24 个字节一定是一个随机数
// 因为下面的 Write 方法第一个包就是放了一个 24 byte 的 nonce 在前面
nonce := make([]byte, 24)
_, err := io.ReadFull(c.Conn, nonce)
if err != nil {
return 0, errShortNonce
}
// salsa20.New 这个实现过程比较复杂,目的是返回一个标准库中 cipher.Stream 接口的实现类
c.readS20 = salsa20.New(c.psk, nonce)
}
maxn := uint32(len(out))
in := mpool.ByteSlicePool.Get(maxn).([]byte) // get buffer
defer mpool.ByteSlicePool.Put(maxn, in) // put the buffer back
in = in[:maxn] // truncate to required length
n, err := c.Conn.Read(in) // read to in
if n > 0 {
// 代码看到这里就可以省略后面的处理了,因为答案已经出来了
// 直接去看标准库中对于 cipher.Stream.XORKeyStream 的定义即可
c.readS20.XORKeyStream(out[:n], in[:n]) // decrypt to out buffer
}
return n, err
}
func (c *pskConn) Write(in []byte) (int, error) {
......
c.writeS20.XORKeyStream(out, in) // encrypt
return c.Conn.Write(out) // send
}
从 Read
和 Write
的实现中看到了 XORKeyStream
,字面能猜出这是通过数据包和 key
进行异或操作而实现的简单的加密传输,两边的key肯定是一致的,并且每次建立连接时都会交换一个 24
位的 nonce
,细心的同学可以去看一下具体的拆分逻辑,分成了 前16 和后 8 ,又做了一些复杂的位移操作用来生成连接通道上使用的key,随机数保证了每次连接通道上的 key 都是不同的。
谁在使用 Protector ?
- 答案:go-libp2p-transport-upgrader
在 libp2p
中定义了很多 Option
用来初始化 Host
对象,在前面的几篇入门文章中提及过,其中就有一个叫 libp2p.PrivateNetwork
的 option
,用来为 Host
指定 Protector
。前面的文章也提到过 TcpTransport
到 Upgrader
的调用过程,这个 Protector
就是在 Upgrader.upgrade
中被使用到的,有兴趣的同学可以重新回忆并翻找一下代码。
总结
现在我们知道了私网是通过对通道进行加密来建立的,发出的数据包都用密钥进行一次异或 XOR(packet,securityKey)
,接收端再做一次相同的操作即可还原数据包。这个原理计算机专业的同学应该都了解,如果不了解就演算一下
- 演算过程: 设 packet = 1 , securityKey = 1
securityPacket = xor(packet, securityKey) = xor(1,1) = 0
packet = xor(securityPacket,securityKey) = xor(0,1) = 1