Skip to main content

karyon_p2p/discovery/kademlia/routing_table/
mod.rs

1mod bucket;
2mod entry;
3
4use std::net::IpAddr;
5
6use parking_lot::RwLock;
7
8use rand::seq::IndexedRandom;
9
10use karyon_net::Addr;
11
12use crate::discovery::kademlia::bloom::LocalBloom;
13
14pub use bucket::{
15    Bucket, BucketEntry, EntryStatusFlag, CONNECTED_ENTRY, DISCONNECTED_ENTRY, PENDING_ENTRY,
16    UNREACHABLE_ENTRY, UNSTABLE_ENTRY,
17};
18pub use entry::{xor_distance, Entry, Key};
19
20use bucket::BUCKET_SIZE;
21use entry::KEY_SIZE;
22
23/// The total number of buckets in the routing table.
24const TABLE_SIZE: usize = 32;
25
26/// The distance limit for the closest buckets.
27const DISTANCE_LIMIT: usize = 32;
28
29/// The maximum number of matched subnets allowed within a single bucket.
30const MAX_MATCHED_SUBNET_IN_BUCKET: usize = 1;
31
32/// The maximum number of matched subnets across the entire routing table.
33const MAX_MATCHED_SUBNET_IN_TABLE: usize = 6;
34
35/// Represents the possible result when adding a new entry.
36#[derive(Debug)]
37pub enum AddEntryResult {
38    /// The entry is added.
39    Added,
40    /// The entry is already exists.
41    Exists,
42    /// The entry is ignored.
43    Ignored,
44    /// The entry is restricted and not allowed.
45    Restricted,
46}
47
48/// This is a modified version of the Kademlia Distributed Hash Table (DHT).
49/// <https://en.wikipedia.org/wiki/Kademlia>
50#[derive(Debug)]
51pub struct RoutingTable {
52    key: Key,
53    buckets: RwLock<Vec<Bucket>>,
54}
55
56impl RoutingTable {
57    /// Creates a new RoutingTable
58    pub fn new(key: Key) -> Self {
59        let buckets: Vec<Bucket> = (0..TABLE_SIZE).map(|_| Bucket::new()).collect();
60        Self {
61            key,
62            buckets: RwLock::new(buckets),
63        }
64    }
65
66    /// Adds a new entry to the table and returns a result indicating success,
67    /// failure, or restrictions.
68    ///
69    /// Takes the write lock once and performs all checks inside it to
70    /// avoid TOCTOU between contains_key / subnet_restricted / add.
71    pub fn add_entry(&self, entry: Entry) -> AddEntryResult {
72        // Determine the index of the bucket where the entry should be placed.
73        let bucket_idx = match self.bucket_index(&entry.key) {
74            Some(i) => i,
75            None => return AddEntryResult::Ignored,
76        };
77
78        let mut buckets = self.buckets.write();
79
80        // Check if the entry already exists in the bucket.
81        if buckets[bucket_idx].contains_key(&entry.key) {
82            return AddEntryResult::Exists;
83        }
84
85        // Check if the entry is restricted.
86        if subnet_restricted(&buckets, bucket_idx, &entry) {
87            return AddEntryResult::Restricted;
88        }
89
90        let bucket = &mut buckets[bucket_idx];
91
92        // If the bucket has free space, add the entry and return success.
93        if bucket.len() < BUCKET_SIZE {
94            bucket.add(&entry);
95            return AddEntryResult::Added;
96        }
97
98        // Replace it with an incompatible entry if one exists.
99        let incompatible_entry = bucket.iter().find(|e| e.is_incompatible()).cloned();
100        if let Some(e) = incompatible_entry {
101            bucket.remove(&e.entry.key);
102            bucket.add(&entry);
103            return AddEntryResult::Added;
104        }
105
106        // If the bucket is full, the entry is ignored.
107        AddEntryResult::Ignored
108    }
109
110    /// Check if the table contains the given key.
111    pub fn contains_key(&self, key: &Key) -> bool {
112        let buckets = self.buckets.read();
113        // Determine the bucket index for the given key.
114        let bucket_idx = match self.bucket_index(key) {
115            Some(bi) => bi,
116            None => return false,
117        };
118
119        let bucket = &buckets[bucket_idx];
120        bucket.contains_key(key)
121    }
122
123    /// Updates the status of an entry in the routing table identified
124    /// by the given key.
125    ///
126    /// If the key is not found, no action is taken.
127    pub fn update_entry(&self, key: &Key, entry_flag: EntryStatusFlag) {
128        let mut buckets = self.buckets.write();
129        // Determine the bucket index for the given key.
130        let bucket_idx = match self.bucket_index(key) {
131            Some(bi) => bi,
132            None => return,
133        };
134
135        let bucket = &mut buckets[bucket_idx];
136        bucket.update_entry(key, entry_flag);
137    }
138
139    /// Returns a list of bucket indexes that are closest to the given target key.
140    pub fn bucket_indexes(&self, target_key: &Key) -> Vec<usize> {
141        let mut indexes = vec![];
142
143        // Determine the primary bucket index for the target key.
144        let bucket_idx = self.bucket_index(target_key).unwrap_or(0);
145
146        indexes.push(bucket_idx);
147
148        // Add additional bucket indexes within a certain distance limit.
149        for i in 1..DISTANCE_LIMIT {
150            if bucket_idx >= i && bucket_idx - i >= 1 {
151                indexes.push(bucket_idx - i);
152            }
153
154            if bucket_idx + i < (TABLE_SIZE - 1) {
155                indexes.push(bucket_idx + i);
156            }
157        }
158
159        indexes
160    }
161
162    /// Returns a list of the closest entries to the given target key, limited by max_entries.
163    pub fn closest_entries(&self, target_key: &Key, max_entries: usize) -> Vec<Entry> {
164        self.closest_entries_filtered(target_key, max_entries, &LocalBloom::default())
165    }
166
167    /// Same as `closest_entries`, but skips entries whose bloom does
168    /// not satisfy `local` (see `LocalBloom::matches`).
169    pub fn closest_entries_filtered(
170        &self,
171        target_key: &Key,
172        max_entries: usize,
173        local: &LocalBloom,
174    ) -> Vec<Entry> {
175        let buckets = self.buckets.read();
176        let mut entries: Vec<Entry> = vec![];
177
178        'outer: for idx in self.bucket_indexes(target_key) {
179            let bucket = &buckets[idx];
180            for bucket_entry in bucket.iter() {
181                if bucket_entry.is_unreachable() || bucket_entry.is_unstable() {
182                    continue;
183                }
184                if !local.matches(&bucket_entry.entry.protocols) {
185                    continue;
186                }
187
188                entries.push(bucket_entry.entry.clone());
189                if entries.len() == max_entries {
190                    break 'outer;
191                }
192            }
193        }
194
195        entries.sort_by(|a, b| {
196            xor_distance(target_key, &a.key).cmp(&xor_distance(target_key, &b.key))
197        });
198
199        entries
200    }
201
202    /// Returns all entries whose advertised bloom may contain `item`.
203    /// Skips unreachable / unstable entries. False positives are
204    /// possible per bloom semantics.
205    pub fn entries_with_item(&self, item: &[u8]) -> Vec<Entry> {
206        let buckets = self.buckets.read();
207        let mut entries = Vec::new();
208        for bucket in buckets.iter() {
209            for be in bucket.iter() {
210                if be.is_unreachable() || be.is_unstable() {
211                    continue;
212                }
213                if be.entry.protocols.may_contain(item) {
214                    entries.push(be.entry.clone());
215                }
216            }
217        }
218        entries
219    }
220
221    /// Removes an entry with the given key from the routing table, if it exists.
222    pub fn remove_entry(&self, key: &Key) {
223        let mut buckets = self.buckets.write();
224        // Determine the bucket index for the given key.
225        let bucket_idx = match self.bucket_index(key) {
226            Some(bi) => bi,
227            None => return,
228        };
229
230        let bucket = &mut buckets[bucket_idx];
231        bucket.remove(key);
232    }
233
234    /// Returns true if any entry has a discovery address with the given IP.
235    /// Used by the refresh listen loop to drop pings from unknown sources.
236    pub fn has_discovery_ip(&self, ip: &IpAddr) -> bool {
237        let buckets = self.buckets.read();
238        for bucket in buckets.iter() {
239            for be in bucket.iter() {
240                for a in be.entry.discovery_addrs.iter() {
241                    if let Addr::Ip(entry_ip) = &a.addr {
242                        if entry_ip == ip {
243                            return true;
244                        }
245                    }
246                }
247            }
248        }
249        false
250    }
251
252    /// Returns up to `per_bucket` entries from each bucket, skipping
253    /// connected and incompatible ones. Used by the refresh service.
254    pub fn refresh_candidates(&self, per_bucket: usize) -> Vec<BucketEntry> {
255        let buckets = self.buckets.read();
256        let mut entries = Vec::new();
257        for bucket in buckets.iter() {
258            for entry in bucket
259                .iter()
260                .filter(|e| !e.is_connected() && !e.is_incompatible())
261                .take(per_bucket)
262            {
263                entries.push(entry.clone());
264            }
265        }
266        entries
267    }
268
269    /// Returns a random entry from the routing table.
270    pub fn random_entry(&self, entry_flag: EntryStatusFlag) -> Option<Entry> {
271        self.random_entry_filtered(entry_flag, &LocalBloom::default())
272    }
273
274    /// Same as `random_entry`, but only returns entries whose bloom
275    /// satisfies `local` (see `LocalBloom::matches`).
276    pub fn random_entry_filtered(
277        &self,
278        entry_flag: EntryStatusFlag,
279        local: &LocalBloom,
280    ) -> Option<Entry> {
281        let buckets = self.buckets.read();
282        for bucket in buckets.choose_multiple(&mut rand::rng(), buckets.len()) {
283            for entry in bucket.random_iter(bucket.len()) {
284                if entry.status & entry_flag == 0 {
285                    continue;
286                }
287                if !local.matches(&entry.entry.protocols) {
288                    continue;
289                }
290                return Some(entry.entry.clone());
291            }
292        }
293
294        None
295    }
296
297    // Returns the bucket index for a given key in the table.
298    fn bucket_index(&self, key: &Key) -> Option<usize> {
299        // Calculate the XOR distance between the self key and the provided key.
300        let distance = xor_distance(&self.key, key);
301
302        for (i, b) in distance.iter().enumerate() {
303            if *b != 0 {
304                let lz = i * 8 + b.leading_zeros() as usize;
305                let bits = KEY_SIZE * 8 - 1;
306                let idx = (bits - lz) / 8;
307                return Some(idx);
308            }
309        }
310        None
311    }
312}
313
314/// Iterate the buckets and count entries in the same subnet as `entry`.
315/// Restricted if same-bucket matches >= MAX_MATCHED_SUBNET_IN_BUCKET or
316/// table-wide matches >= MAX_MATCHED_SUBNET_IN_TABLE.
317///
318/// Takes a borrow of buckets so the caller (add_entry) holds a single
319/// write lock across the check and the insert.
320fn subnet_restricted(buckets: &[Bucket], idx: usize, entry: &Entry) -> bool {
321    let mut bucket_count = 0;
322    let mut table_count = 0;
323
324    for (i, bucket) in buckets.iter().enumerate() {
325        for e in bucket.iter() {
326            let matched = match (e.entry.primary_addr(), entry.primary_addr()) {
327                (Some(a), Some(b)) => subnet_match(a, b),
328                _ => false,
329            };
330            if matched {
331                if i == idx {
332                    bucket_count += 1;
333                }
334                table_count += 1;
335            }
336
337            if bucket_count >= MAX_MATCHED_SUBNET_IN_BUCKET {
338                return true;
339            }
340        }
341
342        if table_count >= MAX_MATCHED_SUBNET_IN_TABLE {
343            return true;
344        }
345    }
346
347    false
348}
349
350/// Check if two addresses belong to the same subnet.
351fn subnet_match(addr: &Addr, other_addr: &Addr) -> bool {
352    // Multiple loopback peers are allowed (e.g. local testing).
353    if is_loopback_pair(addr, other_addr) {
354        return false;
355    }
356    match (addr, other_addr) {
357        // Compare the /24 prefix.
358        (Addr::Ip(IpAddr::V4(ip)), Addr::Ip(IpAddr::V4(other_ip))) => {
359            ip.octets()[0..3] == other_ip.octets()[0..3]
360        }
361        // Compare the /64 prefix (first 4 16-bit segments).
362        (Addr::Ip(IpAddr::V6(ip)), Addr::Ip(IpAddr::V6(other_ip))) => {
363            ip.segments()[0..4] == other_ip.segments()[0..4]
364        }
365        _ => false,
366    }
367}
368
369/// True if both addresses are IPv4 or IPv6 loopback.
370fn is_loopback_pair(a: &Addr, b: &Addr) -> bool {
371    match (a, b) {
372        (Addr::Ip(a), Addr::Ip(b)) => a.is_loopback() && b.is_loopback(),
373        _ => false,
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::bucket::ALL_ENTRY;
380    use super::*;
381
382    use karyon_net::Addr;
383
384    use crate::{
385        discovery::kademlia::bloom::Bloom,
386        message::{PeerAddr, Protocol},
387    };
388
389    struct Setup {
390        local_key: Key,
391        keys: Vec<Key>,
392    }
393
394    fn new_entry(key: &Key, addr: &Addr, port: u16, discovery_port: u16) -> Entry {
395        Entry {
396            key: *key,
397            addrs: vec![PeerAddr {
398                addr: addr.clone(),
399                port,
400                protocol: Protocol::Tcp,
401                priority: 0,
402            }],
403            discovery_addrs: vec![
404                PeerAddr {
405                    addr: addr.clone(),
406                    port: discovery_port,
407                    protocol: Protocol::Tcp,
408                    priority: 0,
409                },
410                PeerAddr {
411                    addr: addr.clone(),
412                    port: discovery_port,
413                    protocol: Protocol::Udp,
414                    priority: 1,
415                },
416            ],
417            protocols: Bloom::default(),
418        }
419    }
420
421    impl Setup {
422        fn new() -> Self {
423            let keys = vec![
424                [
425                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
426                    0, 0, 0, 0, 0, 1,
427                ],
428                [
429                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
430                    1, 1, 0, 1, 1, 2,
431                ],
432                [
433                    0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
434                    0, 0, 0, 0, 0, 3,
435                ],
436                [
437                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 1, 18, 0, 0, 0,
438                    0, 0, 0, 0, 0, 4,
439                ],
440                [
441                    223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
442                    0, 0, 0, 0, 0, 5,
443                ],
444                [
445                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 1, 18, 0, 0, 0,
446                    0, 0, 0, 0, 0, 6,
447                ],
448                [
449                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 50, 1, 18, 0, 0,
450                    0, 0, 0, 0, 0, 0, 7,
451                ],
452                [
453                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 50, 1, 18, 0, 0,
454                    0, 0, 0, 0, 0, 0, 8,
455                ],
456                [
457                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10, 50, 1, 18, 0, 0,
458                    0, 0, 0, 0, 0, 0, 9,
459                ],
460            ];
461
462            Self {
463                local_key: [
464                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
465                    0, 0, 0, 0, 0, 0,
466                ],
467                keys,
468            }
469        }
470
471        fn entries(&self) -> Vec<Entry> {
472            let mut entries = vec![];
473            for (i, key) in self.keys.iter().enumerate() {
474                entries.push(new_entry(
475                    key,
476                    &Addr::Ip(format!("127.0.{i}.1").parse().unwrap()),
477                    3000,
478                    3010,
479                ));
480            }
481            entries
482        }
483
484        fn table(&self) -> RoutingTable {
485            let table = RoutingTable::new(self.local_key);
486
487            for entry in self.entries() {
488                let res = table.add_entry(entry);
489                assert!(matches!(res, AddEntryResult::Added));
490            }
491
492            table
493        }
494    }
495
496    #[test]
497    fn test_bucket_index() {
498        let setup = Setup::new();
499        let table = setup.table();
500
501        assert_eq!(table.bucket_index(&setup.local_key), None);
502        assert_eq!(table.bucket_index(&setup.keys[0]), Some(0));
503        assert_eq!(table.bucket_index(&setup.keys[1]), Some(5));
504        assert_eq!(table.bucket_index(&setup.keys[2]), Some(26));
505        assert_eq!(table.bucket_index(&setup.keys[3]), Some(11));
506        assert_eq!(table.bucket_index(&setup.keys[4]), Some(31));
507        assert_eq!(table.bucket_index(&setup.keys[5]), Some(11));
508        assert_eq!(table.bucket_index(&setup.keys[6]), Some(12));
509        assert_eq!(table.bucket_index(&setup.keys[7]), Some(13));
510        assert_eq!(table.bucket_index(&setup.keys[8]), Some(14));
511    }
512
513    #[test]
514    fn test_closest_entries() {
515        let setup = Setup::new();
516        let table = setup.table();
517        let entries = setup.entries();
518
519        assert_eq!(
520            table.closest_entries(&setup.keys[5], 8),
521            vec![
522                entries[5].clone(),
523                entries[3].clone(),
524                entries[1].clone(),
525                entries[6].clone(),
526                entries[7].clone(),
527                entries[8].clone(),
528                entries[2].clone(),
529            ]
530        );
531
532        assert_eq!(
533            table.closest_entries(&setup.keys[4], 2),
534            vec![entries[4].clone(), entries[2].clone()]
535        );
536    }
537
538    #[test]
539    fn test_random_entry() {
540        let setup = Setup::new();
541        let table = setup.table();
542        let entries = setup.entries();
543
544        let entry = table.random_entry(ALL_ENTRY);
545        assert!(entry.is_some());
546
547        let entry = table.random_entry(CONNECTED_ENTRY);
548        assert!(entry.is_none());
549
550        for entry in entries {
551            table.remove_entry(&entry.key);
552        }
553
554        let entry = table.random_entry(ALL_ENTRY);
555        assert!(entry.is_none());
556    }
557
558    #[test]
559    fn test_add_entries() {
560        let setup = Setup::new();
561        let table = setup.table();
562
563        let key = [
564            0, 0, 0, 0, 0, 0, 0, 1, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
565            0, 0, 5,
566        ];
567
568        let key2 = [
569            0, 0, 0, 0, 0, 0, 0, 1, 2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
570            0, 0, 5,
571        ];
572
573        let entry1 = new_entry(&key, &Addr::Ip("240.120.3.1".parse().unwrap()), 3000, 3010);
574        assert!(matches!(
575            table.add_entry(entry1.clone()),
576            AddEntryResult::Added
577        ));
578
579        assert!(matches!(table.add_entry(entry1), AddEntryResult::Exists));
580
581        let entry2 = new_entry(&key2, &Addr::Ip("240.120.3.2".parse().unwrap()), 3000, 3010);
582        assert!(matches!(
583            table.add_entry(entry2),
584            AddEntryResult::Restricted
585        ));
586
587        let mut key: [u8; 32] = [0; 32];
588
589        for i in 0..BUCKET_SIZE {
590            key[i] += 1;
591            let entry = new_entry(
592                &key,
593                &Addr::Ip(format!("127.0.{i}.1").parse().unwrap()),
594                3000,
595                3010,
596            );
597            table.add_entry(entry);
598        }
599
600        key[BUCKET_SIZE] += 1;
601        let entry = new_entry(&key, &Addr::Ip("125.20.0.1".parse().unwrap()), 3000, 3010);
602        assert!(matches!(table.add_entry(entry), AddEntryResult::Ignored));
603    }
604
605    use std::net::{Ipv4Addr, Ipv6Addr};
606    #[test]
607    fn check_subnet_match() {
608        let addr_v4 = Addr::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
609        let other_addr_v4 = Addr::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)));
610
611        let addr_v6 = Addr::Ip(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)));
612        let other_addr_v6 = Addr::Ip(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2)));
613        let diff_addr_v6 = Addr::Ip(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb7, 0, 0, 0, 0, 0, 2)));
614
615        assert!(subnet_match(&addr_v4, &other_addr_v4));
616        assert!(subnet_match(&addr_v6, &other_addr_v6));
617        assert!(!subnet_match(&addr_v6, &diff_addr_v6));
618    }
619}