2017-12-28 22:16:20 -06:00
package yggdrasil
// This thing manages search packets
// The basic idea is as follows:
// We may know a NodeID (with a mask) and want to connect
2020-03-23 18:03:31 -05:00
// We begin a search by sending a dht lookup to ourself
2020-03-22 18:42:42 -05:00
// Each time a node responds, we sort the results and filter to only include useful nodes
// We then periodically send a packet to the first node from the list (after re-filtering)
// This happens in parallel for each node that replies
// Meanwhile, we keep a list of the (up to) 16 closest nodes to the destination that we've visited
// We only consider an unvisited node useful if either the list isn't full or the unvisited node is closer to the destination than the furthest node on the list
// That gives the search some chance to recover if it hits a dead end where a node doesn't know everyone it should
2018-10-29 22:24:18 -05:00
2018-06-12 17:50:08 -05:00
import (
2019-04-18 23:38:23 +01:00
"errors"
2020-02-06 17:38:42 -06:00
"sort"
2018-06-12 17:50:08 -05:00
"time"
2018-12-14 20:49:18 -06:00
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
2018-06-12 17:50:08 -05:00
)
2017-12-28 22:16:20 -06:00
2020-01-06 18:37:43 -06:00
// This defines the time after which we time out a search (so it can restart).
const search_RETRY_TIME = 3 * time . Second
2020-03-19 21:11:17 -05:00
const search_STEP_TIME = time . Second
const search_MAX_RESULTS = dht_lookup_size
2018-06-02 14:57:06 -05:00
2018-06-10 18:03:28 -05:00
// Information about an ongoing search.
2019-01-09 11:44:45 +02:00
// Includes the target NodeID, the bitmask to match it to an IP, and the list of nodes to visit / already visited.
2017-12-28 22:16:20 -06:00
type searchInfo struct {
2019-08-23 20:42:38 -05:00
searches * searches
2019-04-18 23:38:23 +01:00
dest crypto . NodeID
mask crypto . NodeID
2019-08-11 13:11:14 -05:00
time time . Time
2020-03-19 21:11:17 -05:00
visited [ ] * crypto . NodeID // Closest addresses visited so far
2019-04-18 23:38:23 +01:00
callback func ( * sessionInfo , error )
2019-06-25 19:31:29 -05:00
// TODO context.Context for timeout and cancellation
2020-02-01 13:58:08 -06:00
send uint64 // log number of requests sent
recv uint64 // log number of responses received
2017-12-28 22:16:20 -06:00
}
2018-06-10 18:03:28 -05:00
// This stores a map of active searches.
2017-12-28 22:16:20 -06:00
type searches struct {
2019-08-25 12:10:59 -05:00
router * router
searches map [ crypto . NodeID ] * searchInfo
2017-12-28 22:16:20 -06:00
}
2019-06-25 19:31:29 -05:00
// Initializes the searches struct.
2019-08-23 20:42:38 -05:00
func ( s * searches ) init ( r * router ) {
s . router = r
2018-12-14 20:49:18 -06:00
s . searches = make ( map [ crypto . NodeID ] * searchInfo )
2017-12-28 22:16:20 -06:00
}
2019-08-28 19:31:04 +01:00
func ( s * searches ) reconfigure ( ) {
2019-08-25 12:10:59 -05:00
// This is where reconfiguration would go, if we had anything to do
}
2018-06-10 18:03:28 -05:00
// Creates a new search info, adds it to the searches struct, and returns a pointer to the info.
2019-04-18 23:38:23 +01:00
func ( s * searches ) createSearch ( dest * crypto . NodeID , mask * crypto . NodeID , callback func ( * sessionInfo , error ) ) * searchInfo {
2018-01-04 22:37:51 +00:00
info := searchInfo {
2019-08-23 20:42:38 -05:00
searches : s ,
2019-04-18 23:38:23 +01:00
dest : * dest ,
mask : * mask ,
2019-08-11 13:11:14 -05:00
time : time . Now ( ) ,
2019-04-18 23:38:23 +01:00
callback : callback ,
2018-01-04 22:37:51 +00:00
}
s . searches [ * dest ] = & info
return & info
2017-12-28 22:16:20 -06:00
}
////////////////////////////////////////////////////////////////////////////////
2019-06-25 19:31:29 -05:00
// Checks if there's an ongoing search related to a dhtRes.
2018-06-10 18:03:28 -05:00
// If there is, it adds the response info to the search and triggers a new search step.
// If there's no ongoing search, or we if the dhtRes finished the search (it was from the target node), then don't do anything more.
2019-06-25 19:31:29 -05:00
func ( sinfo * searchInfo ) handleDHTRes ( res * dhtRes ) {
2020-03-19 21:11:17 -05:00
if nfo := sinfo . searches . searches [ sinfo . dest ] ; nfo != sinfo {
return // already done
}
2020-02-06 18:37:58 -06:00
if res != nil {
sinfo . recv ++
if sinfo . checkDHTRes ( res ) {
return // Search finished successfully
}
2020-02-08 20:15:48 -06:00
// Use results to start an additional search thread
infos := append ( [ ] * dhtInfo ( nil ) , res . Infos ... )
infos = sinfo . getAllowedInfos ( infos )
2020-02-08 20:26:37 -06:00
if len ( infos ) > 0 {
sinfo . continueSearch ( infos )
}
2018-06-01 23:34:21 -05:00
}
}
2020-02-08 20:15:48 -06:00
// If there has been no response in too long, then this cleans up the search.
2018-06-10 18:03:28 -05:00
// Otherwise, it pops the closest node to the destination (in keyspace) off of the toVisit list and sends a dht ping.
2020-02-08 20:15:48 -06:00
func ( sinfo * searchInfo ) doSearchStep ( infos [ ] * dhtInfo ) {
if len ( infos ) > 0 {
// Send to the next search target
next := infos [ 0 ]
2019-12-25 18:55:29 -06:00
rq := dhtReqKey { next . key , sinfo . dest }
sinfo . searches . router . dht . addCallback ( & rq , sinfo . handleDHTRes )
sinfo . searches . router . dht . ping ( next , & sinfo . dest )
2020-02-01 13:58:08 -06:00
sinfo . send ++
2019-12-25 18:55:29 -06:00
}
2018-06-01 23:34:21 -05:00
}
2020-02-08 20:15:48 -06:00
// Get a list of search targets that are close enough to the destination to try
// Requires an initial list as input
func ( sinfo * searchInfo ) getAllowedInfos ( infos [ ] * dhtInfo ) [ ] * dhtInfo {
2020-03-19 21:11:17 -05:00
var temp [ ] * dhtInfo
for _ , info := range infos {
if false && len ( sinfo . visited ) < search_MAX_RESULTS {
// We're not full on results yet, so don't block anything yet
} else if ! dht_ordered ( & sinfo . dest , info . getNodeID ( ) , sinfo . visited [ len ( sinfo . visited ) - 1 ] ) {
// Too far away
continue
}
var known bool
for _ , nfo := range sinfo . visited {
if * nfo == * info . getNodeID ( ) {
known = true
break
}
}
if ! known {
temp = append ( temp , info )
}
}
infos = append ( infos [ : 0 ] , temp ... ) // restrict to only the allowed infos
2020-02-08 20:15:48 -06:00
sort . SliceStable ( infos , func ( i , j int ) bool {
// Should return true if i is closer to the destination than j
return dht_ordered ( & sinfo . dest , infos [ i ] . getNodeID ( ) , infos [ j ] . getNodeID ( ) )
2020-03-19 21:11:17 -05:00
} ) // Sort infos to start with the closest
if len ( infos ) > search_MAX_RESULTS {
infos = infos [ : search_MAX_RESULTS ] // Limit max number of infos
2020-02-08 20:15:48 -06:00
}
return infos
}
2020-02-08 20:26:37 -06:00
// Run doSearchStep and schedule another continueSearch to happen after search_RETRY_TIME.
// Must not be called with an empty list of infos
2020-02-08 20:15:48 -06:00
func ( sinfo * searchInfo ) continueSearch ( infos [ ] * dhtInfo ) {
sinfo . doSearchStep ( infos )
2020-02-08 20:26:37 -06:00
infos = infos [ 1 : ] // Remove the node we just tried
// In case there's no response, try the next node in infos later
2020-02-06 18:37:58 -06:00
time . AfterFunc ( search_STEP_TIME , func ( ) {
2019-08-27 19:43:54 -05:00
sinfo . searches . router . Act ( nil , func ( ) {
2019-08-25 17:00:02 -05:00
// FIXME this keeps the search alive forever if not for the searches map, fix that
newSearchInfo := sinfo . searches . searches [ sinfo . dest ]
if newSearchInfo != sinfo {
return
}
2020-02-08 20:15:48 -06:00
// Get good infos here instead of at the top, to make sure we can always start things off with a continueSearch call to ourself
infos = sinfo . getAllowedInfos ( infos )
2020-02-08 20:26:37 -06:00
if len ( infos ) > 0 {
sinfo . continueSearch ( infos )
}
2019-08-25 17:00:02 -05:00
} )
} )
2018-06-01 23:34:21 -05:00
}
2020-02-08 20:15:48 -06:00
// Initially start a search
func ( sinfo * searchInfo ) startSearch ( ) {
loc := sinfo . searches . router . core . switchTable . getLocator ( )
var infos [ ] * dhtInfo
infos = append ( infos , & dhtInfo {
key : sinfo . searches . router . core . boxPub ,
coords : loc . getCoords ( ) ,
} )
2020-02-08 20:26:37 -06:00
// Start the search by asking ourself, useful if we're the destination
sinfo . continueSearch ( infos )
// Start a timer to clean up the search if everything times out
var cleanupFunc func ( )
cleanupFunc = func ( ) {
2020-02-08 20:15:48 -06:00
sinfo . searches . router . Act ( nil , func ( ) {
// FIXME this keeps the search alive forever if not for the searches map, fix that
newSearchInfo := sinfo . searches . searches [ sinfo . dest ]
if newSearchInfo != sinfo {
return
}
2020-02-08 20:26:37 -06:00
elapsed := time . Since ( sinfo . time )
if elapsed > search_RETRY_TIME {
// cleanup
delete ( sinfo . searches . searches , sinfo . dest )
2020-03-19 21:11:17 -05:00
sinfo . searches . router . core . log . Debugln ( "search timeout:" , & sinfo . dest , sinfo . send , sinfo . recv )
2020-02-08 20:26:37 -06:00
sinfo . callback ( nil , errors . New ( "search reached dead end" ) )
return
}
time . AfterFunc ( search_RETRY_TIME - elapsed , cleanupFunc )
} )
}
2020-02-08 20:33:35 -06:00
time . AfterFunc ( search_RETRY_TIME , cleanupFunc )
2020-02-08 20:15:48 -06:00
}
2018-06-10 18:03:28 -05:00
// Calls create search, and initializes the iterative search parts of the struct before returning it.
2020-02-08 20:26:37 -06:00
func ( s * searches ) newIterSearch ( dest * crypto . NodeID , mask * crypto . NodeID , callback func ( * sessionInfo , error ) ) * searchInfo {
2019-04-18 23:38:23 +01:00
sinfo := s . createSearch ( dest , mask , callback )
2020-03-19 21:11:17 -05:00
sinfo . visited = append ( sinfo . visited , & s . router . dht . nodeID )
2020-02-08 20:26:37 -06:00
return sinfo
2018-06-01 23:34:21 -05:00
}
2018-06-10 18:03:28 -05:00
// Checks if a dhtRes is good (called by handleDHTRes).
// If the response is from the target, get/create a session, trigger a session ping, and return true.
// Otherwise return false.
2019-06-25 19:31:29 -05:00
func ( sinfo * searchInfo ) checkDHTRes ( res * dhtRes ) bool {
2020-02-01 13:58:08 -06:00
from := dhtInfo { key : res . Key , coords : res . Coords }
them := from . getNodeID ( )
2020-03-19 21:11:17 -05:00
var known bool
for _ , v := range sinfo . visited {
if * v == * them {
known = true
break
}
}
if ! known {
if len ( sinfo . visited ) < search_MAX_RESULTS || dht_ordered ( & sinfo . dest , them , sinfo . visited [ len ( sinfo . visited ) - 1 ] ) {
// Closer to the destination than the threshold, so update visited
sinfo . searches . router . core . log . Debugln ( "Updating search:" , & sinfo . dest , them , sinfo . send , sinfo . recv )
sinfo . visited = append ( sinfo . visited , them )
sort . SliceStable ( sinfo . visited , func ( i , j int ) bool {
// Should return true if i is closer to the destination than j
return dht_ordered ( & sinfo . dest , sinfo . visited [ i ] , sinfo . visited [ j ] )
} ) // Sort infos to start with the closest
if len ( sinfo . visited ) > search_MAX_RESULTS {
sinfo . visited = sinfo . visited [ : search_MAX_RESULTS ]
}
sinfo . time = time . Now ( )
}
}
2018-12-14 20:49:18 -06:00
var destMasked crypto . NodeID
var themMasked crypto . NodeID
for idx := 0 ; idx < crypto . NodeIDLen ; idx ++ {
2019-06-25 19:31:29 -05:00
destMasked [ idx ] = sinfo . dest [ idx ] & sinfo . mask [ idx ]
themMasked [ idx ] = them [ idx ] & sinfo . mask [ idx ]
2018-06-01 23:34:21 -05:00
}
if themMasked != destMasked {
return false
}
2019-10-12 15:46:56 -05:00
finishSearch := func ( sess * sessionInfo , err error ) {
if sess != nil {
// FIXME (!) replay attacks could mess with coords? Give it a handle (tstamp)?
2019-10-27 19:55:35 -05:00
sess . Act ( sinfo . searches . router , func ( ) { sess . coords = res . Coords } )
2019-10-12 15:46:56 -05:00
sess . ping ( sinfo . searches . router )
}
if err != nil {
sinfo . callback ( nil , err )
} else {
sinfo . callback ( sess , nil )
}
// Cleanup
2020-02-06 20:21:17 -06:00
if _ , isIn := sinfo . searches . searches [ sinfo . dest ] ; isIn {
2020-02-07 22:34:54 -06:00
sinfo . searches . router . core . log . Debugln ( "Finished search:" , & sinfo . dest , sinfo . send , sinfo . recv )
2020-02-06 20:21:17 -06:00
delete ( sinfo . searches . searches , res . Dest )
}
2019-10-12 15:46:56 -05:00
}
2018-06-01 23:34:21 -05:00
// They match, so create a session and send a sessionRequest
2019-10-12 15:46:56 -05:00
var err error
2019-08-23 20:42:38 -05:00
sess , isIn := sinfo . searches . router . sessions . getByTheirPerm ( & res . Key )
2018-06-01 23:34:21 -05:00
if ! isIn {
2019-10-12 15:46:56 -05:00
// Don't already have a session
2019-08-23 20:42:38 -05:00
sess = sinfo . searches . router . sessions . createSession ( & res . Key )
2019-06-25 19:31:29 -05:00
if sess == nil {
2019-10-12 15:46:56 -05:00
err = errors . New ( "session not allowed" )
} else if _ , isIn := sinfo . searches . router . sessions . getByTheirPerm ( & res . Key ) ; ! isIn {
2018-06-01 23:34:21 -05:00
panic ( "This should never happen" )
}
2019-09-01 14:07:00 -05:00
} else {
2019-10-12 15:46:56 -05:00
err = errors . New ( "session already exists" )
2018-06-01 23:34:21 -05:00
}
2019-10-12 15:46:56 -05:00
finishSearch ( sess , err )
2018-06-01 23:34:21 -05:00
return true
}