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
2018-06-02 16:45:45 -05:00
// We begin a search by initializing a list of all nodes in our DHT, sorted by closest to the destination
// We then iteratively ping nodes from the search, marking each pinged node as visited
// We add any unvisited nodes from ping responses to the search, truncating to some maximum search size
// This stops when we either run out of nodes to ping (we hit a dead end where we can't make progress without going back), or we reach the destination
// A new search packet is sent immediately after receiving a response
// A new search packet is sent periodically, once per second, in case a packet was dropped (this slowly causes the search to become parallel if the search doesn't timeout but also doesn't finish within 1 second for whatever reason)
2017-12-28 22:16:20 -06:00
2018-06-12 17:50:08 -05:00
import (
"sort"
"time"
)
2017-12-28 22:16:20 -06:00
2018-06-10 18:03:28 -05:00
// This defines the maximum number of dhtInfo that we keep track of for nodes to query in an ongoing search.
2018-06-02 14:57:06 -05:00
const search_MAX_SEARCH_SIZE = 16
2018-06-10 18:03:28 -05:00
// This defines the time after which we send a new search packet.
// Search packets are sent automatically immediately after a response is received.
// So this allows for timeouts and for long searches to become increasingly parallel.
2018-06-02 14:57:06 -05:00
const search_RETRY_TIME = time . Second
2018-06-10 18:03:28 -05:00
// Information about an ongoing search.
// Includes the targed 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 {
2018-06-02 16:33:58 -05:00
dest NodeID
mask NodeID
2018-06-01 23:34:21 -05:00
time time . Time
packet [ ] byte
toVisit [ ] * dhtInfo
2018-06-02 14:57:06 -05:00
visited map [ NodeID ] bool
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 {
2018-01-04 22:37:51 +00:00
core * Core
searches map [ NodeID ] * searchInfo
2017-12-28 22:16:20 -06:00
}
2018-06-10 18:03:28 -05:00
// Intializes the searches struct.
2017-12-28 22:16:20 -06:00
func ( s * searches ) init ( core * Core ) {
2018-01-04 22:37:51 +00:00
s . core = core
s . searches = make ( map [ NodeID ] * searchInfo )
2017-12-28 22:16:20 -06:00
}
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.
2017-12-28 22:16:20 -06:00
func ( s * searches ) createSearch ( dest * NodeID , mask * NodeID ) * searchInfo {
2018-01-04 22:37:51 +00:00
now := time . Now ( )
for dest , sinfo := range s . searches {
if now . Sub ( sinfo . time ) > time . Minute {
delete ( s . searches , dest )
}
}
info := searchInfo {
2018-06-02 16:33:58 -05:00
dest : * dest ,
mask : * mask ,
2018-01-04 22:37:51 +00:00
time : now . Add ( - time . Second ) ,
}
s . searches [ * dest ] = & info
return & info
2017-12-28 22:16:20 -06:00
}
////////////////////////////////////////////////////////////////////////////////
2018-06-10 18:03:28 -05:00
// Checks if there's an ongoing search relaed to a dhtRes.
// 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.
2018-06-01 23:34:21 -05:00
func ( s * searches ) handleDHTRes ( res * dhtRes ) {
2018-06-02 16:30:05 -05:00
sinfo , isIn := s . searches [ res . Dest ]
2018-06-02 00:16:47 -05:00
if ! isIn || s . checkDHTRes ( sinfo , res ) {
// Either we don't recognize this search, or we just finished it
2018-06-01 23:34:21 -05:00
return
2018-06-02 00:16:47 -05:00
} else {
// Add to the search and continue
s . addToSearch ( sinfo , res )
s . doSearchStep ( sinfo )
2018-06-01 23:34:21 -05:00
}
}
2018-06-10 18:03:28 -05:00
// Adds the information from a dhtRes to an ongoing search.
// Info about a node that has already been visited is not re-added to the search.
// Duplicate information about nodes toVisit is deduplicated (the newest information is kept).
// The toVisit list is sorted in ascending order of keyspace distance from the destination.
2018-06-02 00:16:47 -05:00
func ( s * searches ) addToSearch ( sinfo * searchInfo , res * dhtRes ) {
2018-06-02 00:29:36 -05:00
// Add responses to toVisit if closer to dest than the res node
2018-06-02 16:30:05 -05:00
from := dhtInfo { key : res . Key , coords : res . Coords }
for _ , info := range res . Infos {
2018-06-02 14:57:06 -05:00
if sinfo . visited [ * info . getNodeID ( ) ] {
continue
}
2018-10-20 14:48:07 -05:00
if dht_ordered ( from . getNodeID ( ) , info . getNodeID ( ) , & res . Dest ) {
2018-06-01 23:34:21 -05:00
sinfo . toVisit = append ( sinfo . toVisit , info )
}
}
2018-06-02 00:29:36 -05:00
// Deduplicate
vMap := make ( map [ NodeID ] * dhtInfo )
for _ , info := range sinfo . toVisit {
vMap [ * info . getNodeID ( ) ] = info
}
sinfo . toVisit = sinfo . toVisit [ : 0 ]
for _ , info := range vMap {
sinfo . toVisit = append ( sinfo . toVisit , info )
}
// Sort
2018-06-01 23:34:21 -05:00
sort . SliceStable ( sinfo . toVisit , func ( i , j int ) bool {
2018-10-20 14:48:07 -05:00
return dht_ordered ( sinfo . toVisit [ j ] . getNodeID ( ) , sinfo . toVisit [ i ] . getNodeID ( ) , & res . Dest )
2018-06-01 23:34:21 -05:00
} )
2018-06-02 00:29:36 -05:00
// Truncate to some maximum size
2018-06-02 14:57:06 -05:00
if len ( sinfo . toVisit ) > search_MAX_SEARCH_SIZE {
sinfo . toVisit = sinfo . toVisit [ : search_MAX_SEARCH_SIZE ]
2018-06-02 00:29:36 -05:00
}
2018-06-01 23:34:21 -05:00
}
2018-06-10 18:03:28 -05:00
// If there are no nodes left toVisit, then this cleans up the search.
// Otherwise, it pops the closest node to the destination (in keyspace) off of the toVisit list and sends a dht ping.
2018-06-01 23:34:21 -05:00
func ( s * searches ) doSearchStep ( sinfo * searchInfo ) {
2018-06-02 14:57:06 -05:00
if len ( sinfo . toVisit ) == 0 {
// Dead end, do cleanup
2018-06-02 16:33:58 -05:00
delete ( s . searches , sinfo . dest )
2018-06-01 23:34:21 -05:00
return
} else {
// Send to the next search target
var next * dhtInfo
next , sinfo . toVisit = sinfo . toVisit [ 0 ] , sinfo . toVisit [ 1 : ]
2018-10-20 14:48:07 -05:00
//var oldPings int
//oldPings, next.pings = next.pings, 0
2018-06-02 16:33:58 -05:00
s . core . dht . ping ( next , & sinfo . dest )
2018-10-20 14:48:07 -05:00
//next.pings = oldPings // Don't evict a node for searching with it too much
2018-06-02 14:57:06 -05:00
sinfo . visited [ * next . getNodeID ( ) ] = true
2018-06-01 23:34:21 -05:00
}
}
2018-06-10 18:03:28 -05:00
// If we've recenty sent a ping for this search, do nothing.
// Otherwise, doSearchStep and schedule another continueSearch to happen after search_RETRY_TIME.
2018-06-01 23:34:21 -05:00
func ( s * searches ) continueSearch ( sinfo * searchInfo ) {
2018-06-02 14:57:06 -05:00
if time . Since ( sinfo . time ) < search_RETRY_TIME {
2018-06-01 23:34:21 -05:00
return
}
sinfo . time = time . Now ( )
s . doSearchStep ( sinfo )
2018-06-02 14:57:06 -05:00
// In case the search dies, try to spawn another thread later
// Note that this will spawn multiple parallel searches as time passes
// Any that die aren't restarted, but a new one will start later
retryLater := func ( ) {
2018-06-02 16:33:58 -05:00
newSearchInfo := s . searches [ sinfo . dest ]
2018-06-02 14:57:06 -05:00
if newSearchInfo != sinfo {
return
}
s . continueSearch ( sinfo )
}
go func ( ) {
time . Sleep ( search_RETRY_TIME )
s . core . router . admin <- retryLater
} ( )
2018-06-01 23:34:21 -05:00
}
2018-06-10 18:03:28 -05:00
// Calls create search, and initializes the iterative search parts of the struct before returning it.
2018-06-01 23:34:21 -05:00
func ( s * searches ) newIterSearch ( dest * NodeID , mask * NodeID ) * searchInfo {
sinfo := s . createSearch ( dest , mask )
2018-06-02 16:39:34 -05:00
sinfo . toVisit = s . core . dht . lookup ( dest , true )
2018-06-02 14:57:06 -05:00
sinfo . visited = make ( map [ NodeID ] bool )
2018-06-01 23:34:21 -05:00
return sinfo
}
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.
2018-06-02 00:16:47 -05:00
func ( s * searches ) checkDHTRes ( info * searchInfo , res * dhtRes ) bool {
2018-06-02 16:30:05 -05:00
them := getNodeID ( & res . Key )
2018-06-01 23:34:21 -05:00
var destMasked NodeID
var themMasked NodeID
for idx := 0 ; idx < NodeIDLen ; idx ++ {
destMasked [ idx ] = info . dest [ idx ] & info . mask [ idx ]
themMasked [ idx ] = them [ idx ] & info . mask [ idx ]
}
if themMasked != destMasked {
return false
}
// They match, so create a session and send a sessionRequest
2018-06-02 16:30:05 -05:00
sinfo , isIn := s . core . sessions . getByTheirPerm ( & res . Key )
2018-06-01 23:34:21 -05:00
if ! isIn {
2018-06-02 16:30:05 -05:00
sinfo = s . core . sessions . createSession ( & res . Key )
2018-10-08 19:51:51 +01:00
if sinfo == nil {
// nil if the DHT search finished but the session wasn't allowed
return true
}
2018-06-02 16:30:05 -05:00
_ , isIn := s . core . sessions . getByTheirPerm ( & res . Key )
2018-06-01 23:34:21 -05:00
if ! isIn {
panic ( "This should never happen" )
}
}
// FIXME (!) replay attacks could mess with coords? Give it a handle (tstamp)?
2018-06-02 16:30:05 -05:00
sinfo . coords = res . Coords
2018-06-01 23:34:21 -05:00
sinfo . packet = info . packet
s . core . sessions . ping ( sinfo )
// Cleanup
2018-06-02 16:30:05 -05:00
delete ( s . searches , res . Dest )
2018-06-01 23:34:21 -05:00
return true
}