github.com/linapex/ethereum-go-chinese@v0.0.0-20190316121929-f8b7a73c3fa1/p2p/discover/udp.go (about) 1 2 //<developer> 3 // <name>linapex 曹一峰</name> 4 // <email>linapex@163.com</email> 5 // <wx>superexc</wx> 6 // <qqgroup>128148617</qqgroup> 7 // <url>https://jsq.ink</url> 8 // <role>pku engineer</role> 9 // <date>2019-03-16 19:16:41</date> 10 //</624450103171616768> 11 12 13 package discover 14 15 import ( 16 "bytes" 17 "container/list" 18 "crypto/ecdsa" 19 "errors" 20 "fmt" 21 "net" 22 "sync" 23 "time" 24 25 "github.com/ethereum/go-ethereum/crypto" 26 "github.com/ethereum/go-ethereum/log" 27 "github.com/ethereum/go-ethereum/p2p/enode" 28 "github.com/ethereum/go-ethereum/p2p/netutil" 29 "github.com/ethereum/go-ethereum/rlp" 30 ) 31 32 //错误 33 var ( 34 errPacketTooSmall = errors.New("too small") 35 errBadHash = errors.New("bad hash") 36 errExpired = errors.New("expired") 37 errUnsolicitedReply = errors.New("unsolicited reply") 38 errUnknownNode = errors.New("unknown node") 39 errTimeout = errors.New("RPC timeout") 40 errClockWarp = errors.New("reply deadline too far in the future") 41 errClosed = errors.New("socket closed") 42 ) 43 44 //超时 45 const ( 46 respTimeout = 500 * time.Millisecond 47 expiration = 20 * time.Second 48 bondExpiration = 24 * time.Hour 49 50 ntpFailureThreshold = 32 //连续超时,之后检查NTP 51 ntpWarningCooldown = 10 * time.Minute //重复NTP警告之前要经过的最短时间 52 driftThreshold = 10 * time.Second //警告用户前允许的时钟漂移 53 ) 54 55 //RPC数据包类型 56 const ( 57 pingPacket = iota + 1 //零为“保留” 58 pongPacket 59 findnodePacket 60 neighborsPacket 61 ) 62 63 //RPC请求结构 64 type ( 65 ping struct { 66 Version uint 67 From, To rpcEndpoint 68 Expiration uint64 69 //忽略其他字段(为了向前兼容)。 70 Rest []rlp.RawValue `rlp:"tail"` 71 } 72 73 //乒乓球是对乒乓球的回应。 74 pong struct { 75 //此字段应镜像UDP信封地址 76 //提供了一种发现 77 //外部地址(在NAT之后)。 78 To rpcEndpoint 79 80 ReplyTok []byte //这包含ping包的哈希。 81 Expiration uint64 //数据包失效的绝对时间戳。 82 //忽略其他字段(为了向前兼容)。 83 Rest []rlp.RawValue `rlp:"tail"` 84 } 85 86 //findnode是对接近给定目标的节点的查询。 87 findnode struct { 88 Target encPubkey 89 Expiration uint64 90 //忽略其他字段(为了向前兼容)。 91 Rest []rlp.RawValue `rlp:"tail"` 92 } 93 94 //回复findnode 95 neighbors struct { 96 Nodes []rpcNode 97 Expiration uint64 98 //忽略其他字段(为了向前兼容)。 99 Rest []rlp.RawValue `rlp:"tail"` 100 } 101 102 rpcNode struct { 103 IP net.IP //IPv4的len 4或IPv6的len 16 104 UDP uint16 //用于发现协议 105 TCP uint16 //对于RLPX协议 106 ID encPubkey 107 } 108 109 rpcEndpoint struct { 110 IP net.IP //IPv4的len 4或IPv6的len 16 111 UDP uint16 //用于发现协议 112 TCP uint16 //对于RLPX协议 113 } 114 ) 115 116 func makeEndpoint(addr *net.UDPAddr, tcpPort uint16) rpcEndpoint { 117 ip := net.IP{} 118 if ip4 := addr.IP.To4(); ip4 != nil { 119 ip = ip4 120 } else if ip6 := addr.IP.To16(); ip6 != nil { 121 ip = ip6 122 } 123 return rpcEndpoint{IP: ip, UDP: uint16(addr.Port), TCP: tcpPort} 124 } 125 126 func (t *udp) nodeFromRPC(sender *net.UDPAddr, rn rpcNode) (*node, error) { 127 if rn.UDP <= 1024 { 128 return nil, errors.New("low port") 129 } 130 if err := netutil.CheckRelayIP(sender.IP, rn.IP); err != nil { 131 return nil, err 132 } 133 if t.netrestrict != nil && !t.netrestrict.Contains(rn.IP) { 134 return nil, errors.New("not contained in netrestrict whitelist") 135 } 136 key, err := decodePubkey(rn.ID) 137 if err != nil { 138 return nil, err 139 } 140 n := wrapNode(enode.NewV4(key, rn.IP, int(rn.TCP), int(rn.UDP))) 141 err = n.ValidateComplete() 142 return n, err 143 } 144 145 func nodeToRPC(n *node) rpcNode { 146 var key ecdsa.PublicKey 147 var ekey encPubkey 148 if err := n.Load((*enode.Secp256k1)(&key)); err == nil { 149 ekey = encodePubkey(&key) 150 } 151 return rpcNode{ID: ekey, IP: n.IP(), UDP: uint16(n.UDP()), TCP: uint16(n.TCP())} 152 } 153 154 type packet interface { 155 handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error 156 name() string 157 } 158 159 type conn interface { 160 ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) 161 WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) 162 Close() error 163 LocalAddr() net.Addr 164 } 165 166 //UDP实现Discovery v4 UDP有线协议。 167 type udp struct { 168 conn conn 169 netrestrict *netutil.Netlist 170 priv *ecdsa.PrivateKey 171 localNode *enode.LocalNode 172 db *enode.DB 173 tab *Table 174 wg sync.WaitGroup 175 176 addpending chan *pending 177 gotreply chan reply 178 closing chan struct{} 179 } 180 181 //挂起表示挂起的答复。 182 // 183 //协议的某些实现希望发送多个 184 //将数据包回复到findnode。一般来说,任何邻居包都不能 185 //与特定的findnode包匹配。 186 // 187 //我们的实现通过存储 188 //每个等待答复。来自节点的传入数据包被调度 189 //到该节点的所有回调函数。 190 type pending struct { 191 //这些字段必须在答复中匹配。 192 from enode.ID 193 ptype byte 194 195 //请求必须完成的时间 196 deadline time.Time 197 198 //当匹配的答复到达时调用回调。如果它回来 199 //如果为true,则从挂起的答复队列中删除回调。 200 //如果返回错误,则认为答复不完整,并且 201 //将为下一个匹配的答复再次调用回调。 202 callback func(resp interface{}) (done bool) 203 204 //当回调指示完成或 205 //如果在超时时间内没有收到进一步的答复,则出错。 206 errc chan<- error 207 } 208 209 type reply struct { 210 from enode.ID 211 ptype byte 212 data interface{} 213 //循环指示是否存在 214 //通过此频道发送的匹配请求。 215 matched chan<- bool 216 } 217 218 //无法处理readpacket时,会将其发送到未处理的通道。 219 type ReadPacket struct { 220 Data []byte 221 Addr *net.UDPAddr 222 } 223 224 //配置保存与表相关的设置。 225 type Config struct { 226 //需要这些设置并配置UDP侦听器: 227 PrivateKey *ecdsa.PrivateKey 228 229 //这些设置是可选的: 230 NetRestrict *netutil.Netlist //网络白名单 231 Bootnodes []*enode.Node //引导程序节点列表 232 Unhandled chan<- ReadPacket //在此通道上发送未处理的数据包 233 } 234 235 //listenudp返回一个新表,用于侦听laddr上的udp包。 236 func ListenUDP(c conn, ln *enode.LocalNode, cfg Config) (*Table, error) { 237 tab, _, err := newUDP(c, ln, cfg) 238 if err != nil { 239 return nil, err 240 } 241 return tab, nil 242 } 243 244 func newUDP(c conn, ln *enode.LocalNode, cfg Config) (*Table, *udp, error) { 245 udp := &udp{ 246 conn: c, 247 priv: cfg.PrivateKey, 248 netrestrict: cfg.NetRestrict, 249 localNode: ln, 250 db: ln.Database(), 251 closing: make(chan struct{}), 252 gotreply: make(chan reply), 253 addpending: make(chan *pending), 254 } 255 tab, err := newTable(udp, ln.Database(), cfg.Bootnodes) 256 if err != nil { 257 return nil, nil, err 258 } 259 udp.tab = tab 260 261 udp.wg.Add(2) 262 go udp.loop() 263 go udp.readLoop(cfg.Unhandled) 264 return udp.tab, udp, nil 265 } 266 267 func (t *udp) self() *enode.Node { 268 return t.localNode.Node() 269 } 270 271 func (t *udp) close() { 272 close(t.closing) 273 t.conn.Close() 274 t.wg.Wait() 275 } 276 277 func (t *udp) ourEndpoint() rpcEndpoint { 278 n := t.self() 279 a := &net.UDPAddr{IP: n.IP(), Port: n.UDP()} 280 return makeEndpoint(a, uint16(n.TCP())) 281 } 282 283 //ping向给定节点发送ping消息并等待答复。 284 func (t *udp) ping(toid enode.ID, toaddr *net.UDPAddr) error { 285 return <-t.sendPing(toid, toaddr, nil) 286 } 287 288 //发送ping向给定节点发送ping消息并调用回调 289 //当回复到达时。 290 func (t *udp) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) <-chan error { 291 req := &ping{ 292 Version: 4, 293 From: t.ourEndpoint(), 294 To: makeEndpoint(toaddr, 0), //TODO:可能使用数据库中已知的TCP端口 295 Expiration: uint64(time.Now().Add(expiration).Unix()), 296 } 297 packet, hash, err := encodePacket(t.priv, pingPacket, req) 298 if err != nil { 299 errc := make(chan error, 1) 300 errc <- err 301 return errc 302 } 303 errc := t.pending(toid, pongPacket, func(p interface{}) bool { 304 ok := bytes.Equal(p.(*pong).ReplyTok, hash) 305 if ok && callback != nil { 306 callback() 307 } 308 return ok 309 }) 310 t.localNode.UDPContact(toaddr) 311 t.write(toaddr, req.name(), packet) 312 return errc 313 } 314 315 func (t *udp) waitping(from enode.ID) error { 316 return <-t.pending(from, pingPacket, func(interface{}) bool { return true }) 317 } 318 319 //findnode向给定节点发送findnode请求,并等待直到 320 //节点已发送到k个邻居。 321 func (t *udp) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error) { 322 //如果我们有一段时间没有看到目标节点的ping,它将不会记得 323 //我们的端点证明和拒绝findnode。先打个乒乓球。 324 if time.Since(t.db.LastPingReceived(toid)) > bondExpiration { 325 t.ping(toid, toaddr) 326 t.waitping(toid) 327 } 328 329 nodes := make([]*node, 0, bucketSize) 330 nreceived := 0 331 errc := t.pending(toid, neighborsPacket, func(r interface{}) bool { 332 reply := r.(*neighbors) 333 for _, rn := range reply.Nodes { 334 nreceived++ 335 n, err := t.nodeFromRPC(toaddr, rn) 336 if err != nil { 337 log.Trace("Invalid neighbor node received", "ip", rn.IP, "addr", toaddr, "err", err) 338 continue 339 } 340 nodes = append(nodes, n) 341 } 342 return nreceived >= bucketSize 343 }) 344 t.send(toaddr, findnodePacket, &findnode{ 345 Target: target, 346 Expiration: uint64(time.Now().Add(expiration).Unix()), 347 }) 348 return nodes, <-errc 349 } 350 351 //挂起向挂起的答复队列添加答复回调。 352 //有关详细说明,请参阅“挂起”类型的文档。 353 func (t *udp) pending(id enode.ID, ptype byte, callback func(interface{}) bool) <-chan error { 354 ch := make(chan error, 1) 355 p := &pending{from: id, ptype: ptype, callback: callback, errc: ch} 356 select { 357 case t.addpending <- p: 358 //循环将处理它 359 case <-t.closing: 360 ch <- errClosed 361 } 362 return ch 363 } 364 365 func (t *udp) handleReply(from enode.ID, ptype byte, req packet) bool { 366 matched := make(chan bool, 1) 367 select { 368 case t.gotreply <- reply{from, ptype, req, matched}: 369 //循环将处理它 370 return <-matched 371 case <-t.closing: 372 return false 373 } 374 } 375 376 //循环在自己的Goroutine中运行。它跟踪 377 //刷新计时器和挂起的答复队列。 378 func (t *udp) loop() { 379 defer t.wg.Done() 380 381 var ( 382 plist = list.New() 383 timeout = time.NewTimer(0) 384 nextTimeout *pending //上次重置超时时的plist头 385 contTimeouts = 0 //要执行NTP检查的连续超时数 386 ntpWarnTime = time.Unix(0, 0) 387 ) 388 <-timeout.C //忽略第一次超时 389 defer timeout.Stop() 390 391 resetTimeout := func() { 392 if plist.Front() == nil || nextTimeout == plist.Front().Value { 393 return 394 } 395 //启动计时器,以便在下一个挂起的答复过期时触发。 396 now := time.Now() 397 for el := plist.Front(); el != nil; el = el.Next() { 398 nextTimeout = el.Value.(*pending) 399 if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout { 400 timeout.Reset(dist) 401 return 402 } 403 //删除截止时间太长的挂起答复 404 //未来。如果系统时钟跳变,就会发生这种情况。 405 //在最后期限被分配后向后。 406 nextTimeout.errc <- errClockWarp 407 plist.Remove(el) 408 } 409 nextTimeout = nil 410 timeout.Stop() 411 } 412 413 for { 414 resetTimeout() 415 416 select { 417 case <-t.closing: 418 for el := plist.Front(); el != nil; el = el.Next() { 419 el.Value.(*pending).errc <- errClosed 420 } 421 return 422 423 case p := <-t.addpending: 424 p.deadline = time.Now().Add(respTimeout) 425 plist.PushBack(p) 426 427 case r := <-t.gotreply: 428 var matched bool 429 for el := plist.Front(); el != nil; el = el.Next() { 430 p := el.Value.(*pending) 431 if p.from == r.from && p.ptype == r.ptype { 432 matched = true 433 //如果Matcher的回调指示 434 //所有答复都已收到。这是 435 //需要多个数据包类型 436 //应答包。 437 if p.callback(r.data) { 438 p.errc <- nil 439 plist.Remove(el) 440 } 441 //重置连续超时计数器(时间漂移检测) 442 contTimeouts = 0 443 } 444 } 445 r.matched <- matched 446 447 case now := <-timeout.C: 448 nextTimeout = nil 449 450 //通知并删除期限已过的回调。 451 for el := plist.Front(); el != nil; el = el.Next() { 452 p := el.Value.(*pending) 453 if now.After(p.deadline) || now.Equal(p.deadline) { 454 p.errc <- errTimeout 455 plist.Remove(el) 456 contTimeouts++ 457 } 458 } 459 //如果我们累积了太多超时,请执行NTP时间同步检查 460 if contTimeouts > ntpFailureThreshold { 461 if time.Since(ntpWarnTime) >= ntpWarningCooldown { 462 ntpWarnTime = time.Now() 463 go checkClockDrift() 464 } 465 contTimeouts = 0 466 } 467 } 468 } 469 } 470 471 const ( 472 macSize = 256 / 8 473 sigSize = 520 / 8 474 headSize = macSize + sigSize //包帧数据空间 475 ) 476 477 var ( 478 headSpace = make([]byte, headSize) 479 480 //邻居答复通过多个数据包发送到 481 //低于1280字节的限制。我们计算最大数 482 //通过填充一个包直到它变得太大。 483 maxNeighbors int 484 ) 485 486 func init() { 487 p := neighbors{Expiration: ^uint64(0)} 488 maxSizeNode := rpcNode{IP: make(net.IP, 16), UDP: ^uint16(0), TCP: ^uint16(0)} 489 for n := 0; ; n++ { 490 p.Nodes = append(p.Nodes, maxSizeNode) 491 size, _, err := rlp.EncodeToReader(p) 492 if err != nil { 493 //如果发生这种情况,它将被单元测试捕获。 494 panic("cannot encode: " + err.Error()) 495 } 496 if headSize+size+1 >= 1280 { 497 maxNeighbors = n 498 break 499 } 500 } 501 } 502 503 func (t *udp) send(toaddr *net.UDPAddr, ptype byte, req packet) ([]byte, error) { 504 packet, hash, err := encodePacket(t.priv, ptype, req) 505 if err != nil { 506 return hash, err 507 } 508 return hash, t.write(toaddr, req.name(), packet) 509 } 510 511 func (t *udp) write(toaddr *net.UDPAddr, what string, packet []byte) error { 512 _, err := t.conn.WriteToUDP(packet, toaddr) 513 log.Trace(">> "+what, "addr", toaddr, "err", err) 514 return err 515 } 516 517 func encodePacket(priv *ecdsa.PrivateKey, ptype byte, req interface{}) (packet, hash []byte, err error) { 518 b := new(bytes.Buffer) 519 b.Write(headSpace) 520 b.WriteByte(ptype) 521 if err := rlp.Encode(b, req); err != nil { 522 log.Error("Can't encode discv4 packet", "err", err) 523 return nil, nil, err 524 } 525 packet = b.Bytes() 526 sig, err := crypto.Sign(crypto.Keccak256(packet[headSize:]), priv) 527 if err != nil { 528 log.Error("Can't sign discv4 packet", "err", err) 529 return nil, nil, err 530 } 531 copy(packet[macSize:], sig) 532 //将哈希添加到前面。注意:这不保护 533 //以任何方式打包。我们的公钥将是这个哈希的一部分 534 //未来。 535 hash = crypto.Keccak256(packet[macSize:]) 536 copy(packet, hash) 537 return packet, hash, nil 538 } 539 540 //readloop在自己的goroutine中运行。它处理传入的UDP数据包。 541 func (t *udp) readLoop(unhandled chan<- ReadPacket) { 542 defer t.wg.Done() 543 if unhandled != nil { 544 defer close(unhandled) 545 } 546 547 //发现数据包被定义为不大于1280字节。 548 //大于此尺寸的包装将在末端切割并处理 549 //因为它们的哈希不匹配而无效。 550 buf := make([]byte, 1280) 551 for { 552 nbytes, from, err := t.conn.ReadFromUDP(buf) 553 if netutil.IsTemporaryError(err) { 554 //忽略临时读取错误。 555 log.Debug("Temporary UDP read error", "err", err) 556 continue 557 } else if err != nil { 558 //关闭永久错误循环。 559 log.Debug("UDP read error", "err", err) 560 return 561 } 562 if t.handlePacket(from, buf[:nbytes]) != nil && unhandled != nil { 563 select { 564 case unhandled <- ReadPacket{buf[:nbytes], from}: 565 default: 566 } 567 } 568 } 569 } 570 571 func (t *udp) handlePacket(from *net.UDPAddr, buf []byte) error { 572 packet, fromID, hash, err := decodePacket(buf) 573 if err != nil { 574 log.Debug("Bad discv4 packet", "addr", from, "err", err) 575 return err 576 } 577 err = packet.handle(t, from, fromID, hash) 578 log.Trace("<< "+packet.name(), "addr", from, "err", err) 579 return err 580 } 581 582 func decodePacket(buf []byte) (packet, encPubkey, []byte, error) { 583 if len(buf) < headSize+1 { 584 return nil, encPubkey{}, nil, errPacketTooSmall 585 } 586 hash, sig, sigdata := buf[:macSize], buf[macSize:headSize], buf[headSize:] 587 shouldhash := crypto.Keccak256(buf[macSize:]) 588 if !bytes.Equal(hash, shouldhash) { 589 return nil, encPubkey{}, nil, errBadHash 590 } 591 fromKey, err := recoverNodeKey(crypto.Keccak256(buf[headSize:]), sig) 592 if err != nil { 593 return nil, fromKey, hash, err 594 } 595 596 var req packet 597 switch ptype := sigdata[0]; ptype { 598 case pingPacket: 599 req = new(ping) 600 case pongPacket: 601 req = new(pong) 602 case findnodePacket: 603 req = new(findnode) 604 case neighborsPacket: 605 req = new(neighbors) 606 default: 607 return nil, fromKey, hash, fmt.Errorf("unknown type: %d", ptype) 608 } 609 s := rlp.NewStream(bytes.NewReader(sigdata[1:]), 0) 610 err = s.Decode(req) 611 return req, fromKey, hash, err 612 } 613 614 func (req *ping) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error { 615 if expired(req.Expiration) { 616 return errExpired 617 } 618 key, err := decodePubkey(fromKey) 619 if err != nil { 620 return fmt.Errorf("invalid public key: %v", err) 621 } 622 t.send(from, pongPacket, &pong{ 623 To: makeEndpoint(from, req.From.TCP), 624 ReplyTok: mac, 625 Expiration: uint64(time.Now().Add(expiration).Unix()), 626 }) 627 n := wrapNode(enode.NewV4(key, from.IP, int(req.From.TCP), from.Port)) 628 t.handleReply(n.ID(), pingPacket, req) 629 if time.Since(t.db.LastPongReceived(n.ID())) > bondExpiration { 630 t.sendPing(n.ID(), from, func() { t.tab.addThroughPing(n) }) 631 } else { 632 t.tab.addThroughPing(n) 633 } 634 t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}) 635 t.db.UpdateLastPingReceived(n.ID(), time.Now()) 636 return nil 637 } 638 639 func (req *ping) name() string { return "PING/v4" } 640 641 func (req *pong) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error { 642 if expired(req.Expiration) { 643 return errExpired 644 } 645 fromID := fromKey.id() 646 if !t.handleReply(fromID, pongPacket, req) { 647 return errUnsolicitedReply 648 } 649 t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}) 650 t.db.UpdateLastPongReceived(fromID, time.Now()) 651 return nil 652 } 653 654 func (req *pong) name() string { return "PONG/v4" } 655 656 func (req *findnode) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error { 657 if expired(req.Expiration) { 658 return errExpired 659 } 660 fromID := fromKey.id() 661 if time.Since(t.db.LastPongReceived(fromID)) > bondExpiration { 662 //不存在端点验证pong,我们不处理数据包。这可以防止 663 //攻击向量,发现协议可用于放大 664 //DDoS攻击。恶意参与者将使用IP地址发送findnode请求 665 //目标的UDP端口作为源地址。findnode的接收者 666 //然后,包将发送一个邻居包(比 667 //找到受害者。 668 return errUnknownNode 669 } 670 target := enode.ID(crypto.Keccak256Hash(req.Target[:])) 671 t.tab.mutex.Lock() 672 closest := t.tab.closest(target, bucketSize).entries 673 t.tab.mutex.Unlock() 674 675 p := neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())} 676 var sent bool 677 //以块形式发送邻居,每个数据包最多有maxneighbors 678 //低于1280字节的限制。 679 for _, n := range closest { 680 if netutil.CheckRelayIP(from.IP, n.IP()) == nil { 681 p.Nodes = append(p.Nodes, nodeToRPC(n)) 682 } 683 if len(p.Nodes) == maxNeighbors { 684 t.send(from, neighborsPacket, &p) 685 p.Nodes = p.Nodes[:0] 686 sent = true 687 } 688 } 689 if len(p.Nodes) > 0 || !sent { 690 t.send(from, neighborsPacket, &p) 691 } 692 return nil 693 } 694 695 func (req *findnode) name() string { return "FINDNODE/v4" } 696 697 func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error { 698 if expired(req.Expiration) { 699 return errExpired 700 } 701 if !t.handleReply(fromKey.id(), neighborsPacket, req) { 702 return errUnsolicitedReply 703 } 704 return nil 705 } 706 707 func (req *neighbors) name() string { return "NEIGHBORS/v4" } 708 709 func expired(ts uint64) bool { 710 return time.Unix(int64(ts), 0).Before(time.Now()) 711 } 712