package yggdrasil import ( "encoding/hex" "errors" "time" "github.com/yggdrasil-network/yggdrasil-go/src/crypto" "github.com/yggdrasil-network/yggdrasil-go/src/util" ) type Conn struct { core *Core nodeID *crypto.NodeID nodeMask *crypto.NodeID session *sessionInfo readDeadline time.Time writeDeadline time.Time } // This method should only be called from the router goroutine func (c *Conn) startSearch() { searchCompleted := func(sinfo *sessionInfo, err error) { if err != nil { c.core.log.Debugln("DHT search failed:", err) return } if sinfo != nil { c.session = sinfo c.core.log.Println("Search from API found", hex.EncodeToString(sinfo.theirPermPub[:])) } } // Try and search for the node on the network doSearch := func() { sinfo, isIn := c.core.searches.searches[*c.nodeID] if !isIn { c.core.log.Debugln("Starting search for", hex.EncodeToString(c.nodeID[:])) sinfo = c.core.searches.newIterSearch(c.nodeID, c.nodeMask, searchCompleted) } c.core.searches.continueSearch(sinfo) } var sinfo *sessionInfo var isIn bool switch { case !isIn || !sinfo.init: // No or unintiialized session, so we need to search first doSearch() case time.Since(sinfo.time) > 6*time.Second: if sinfo.time.Before(sinfo.pingTime) && time.Since(sinfo.pingTime) > 6*time.Second { // We haven't heard from the dest in a while // We tried pinging but didn't get a response // They may have changed coords // Try searching to discover new coords // Note that search spam is throttled internally doSearch() } else { // We haven't heard about the dest in a while now := time.Now() if !sinfo.time.Before(sinfo.pingTime) { // Update pingTime to start the clock for searches (above) sinfo.pingTime = now } if time.Since(sinfo.pingSend) > time.Second { // Send at most 1 ping per second sinfo.pingSend = now c.core.sessions.sendPingPong(sinfo, false) } } } } func (c *Conn) Read(b []byte) (int, error) { if c.session == nil { return 0, errors.New("session not open") } p := <-c.session.recv defer util.PutBytes(p.Payload) if !c.session.nonceIsOK(&p.Nonce) { return 0, errors.New("packet dropped due to invalid nonce") } bs, isOK := crypto.BoxOpen(&c.session.sharedSesKey, p.Payload, &p.Nonce) if !isOK { util.PutBytes(bs) return 0, errors.New("packet dropped due to decryption failure") } b = b[:0] b = append(b, bs...) c.session.updateNonce(&p.Nonce) c.session.time = time.Now() c.session.bytesRecvd += uint64(len(bs)) return len(b), nil } func (c *Conn) Write(b []byte) (int, error) { if c.session == nil { c.core.router.doAdmin(func() { c.startSearch() }) return 0, errors.New("session not open") } defer util.PutBytes(b) if !c.session.init { // To prevent using empty session keys return 0, errors.New("session not initialised") } // code isn't multithreaded so appending to this is safe coords := c.session.coords // Prepare the payload payload, nonce := crypto.BoxSeal(&c.session.sharedSesKey, b, &c.session.myNonce) defer util.PutBytes(payload) p := wire_trafficPacket{ Coords: coords, Handle: c.session.theirHandle, Nonce: *nonce, Payload: payload, } packet := p.encode() c.session.bytesSent += uint64(len(b)) c.session.send <- packet c.session.core.router.out(packet) return len(b), nil } func (c *Conn) Close() error { return nil } func (c *Conn) LocalAddr() crypto.NodeID { return *crypto.GetNodeID(&c.session.core.boxPub) } func (c *Conn) RemoteAddr() crypto.NodeID { return *crypto.GetNodeID(&c.session.theirPermPub) } func (c *Conn) SetDeadline(t time.Time) error { c.SetReadDeadline(t) c.SetWriteDeadline(t) return nil } func (c *Conn) SetReadDeadline(t time.Time) error { c.readDeadline = t return nil } func (c *Conn) SetWriteDeadline(t time.Time) error { c.writeDeadline = t return nil }