diff --git a/README.md b/README.md index 1d877ff..16743f0 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,14 @@ This is yet another [LDK](https://lightningdevkit.org/) [demo](https://github.co The current goal of this tool is to determine whether a lightning node has connectivity issues just by listening to gossip messages. -Trick is that peers disable their channel with a node as soon as TCP connection breaks. So if more than n (configurable threshold) nodes disable a channel with a specific node we can tell with a high probability that the given lightning node is unavailable. - +Trick is that peers disable their channel with a node as soon as TCP connection breaks. So if more than n (configurable threshold) nodes disable a channel with a specific node we can tell with a high probability that it is unavailable. All without sending a single probe packet to the target. The tool is completely stateless, everything it needs is queried through `QueryShortChannelIds` and cached later on. -## Fingerprinting Lightning implementations +## Fingerprinting lightning implementations -While developing this I have found a simple way to fingerprint lightnig node implementations. +While developing this I have found a simple way to fingerprint lightning node implementations. * LND internally creates a `GossipSyncer` for the connection only when node advertises it understands `gossip_queries` [BOLT-09](https://github.com/lightning/bolts/blob/master/09-features.md) So if you don't do that `QueryShortChannelIds` will not yield any reply. All other implementations do not need this feature advertised. @@ -33,3 +32,46 @@ You can send something like and stil get a normal reply. * CLN: does not do any of the previous things (which is unique too) + +## Example + +``` +2023-08-30 17:16:52.156 INFO [gossiper::voter:73] THRESHOLD BREACHED num: 28 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 836085134498594816 direction: 1 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 INFO [gossiper::voter:73] THRESHOLD BREACHED num: 28 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 819663928352047104 direction: 1 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 820341227486904321 direction: 1 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 INFO [gossiper::voter:73] THRESHOLD BREACHED num: 28 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 882218443410571264 direction: 1 node: 03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61 alias: node201.fmt.mempool.space + +2023-08-30 17:16:52.157 INFO [gossiper::voter:73] THRESHOLD BREACHED num: 28 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 849020888796364801 direction: 0 node: 02ad4afb6e50ae4635ec5ddf5a57c44d4cc4b376ac6580f78cda0454a86e5fa6c2 alias: wyssblitz + +2023-08-30 17:16:52.157 INFO [gossiper::voter:73] THRESHOLD BREACHED num: 29 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 857100100212817921 direction: 0 node: 021e3544a3b10379e833b85600f05de6d162a8ff93945eebd7def493fc024f1962 alias: TheFlashâš¡ + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 829880590372896769 direction: 0 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 819762884369842176 direction: 1 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 INFO [gossiper::voter:73] THRESHOLD BREACHED num: 29 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:100] ENABLE chid: 749630535234158592 direction: 1 node: 03c445275ee7d79ee5778ca2ac81b7c4d84aed7ee04629e8d8f35434b3c21e2da8 alias: Obi-Wan Cryptobi + +2023-08-30 17:16:52.157 INFO [gossiper::voter:73] THRESHOLD BREACHED num: 30 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom + +2023-08-30 17:16:52.157 TRACE [gossiper::voter:50] DISABLE chid: 820341227486904321 direction: 0 node: 025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5 alias: Moon (paywithmoon.com) + +2023-08-30 17:16:52.157 INFO [gossiper::voter:73] THRESHOLD BREACHED num: 30 node: 036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548 alias: Dawn of Freedom +``` + +Currently the output is very crude but you can detect that `036d306e01a65128e5130c99a004119f36bc296ee6f002a7c3c478530b8249a548` actually seemed to have some issues during testing. +TODO: Need to fix the false positives that come from bigger nodes where a lot of people simultaneously have connectivity issues independently. Since this is stateless it is hard to tell number of channels for a node (unless I do a complete sync but that is slow). diff --git a/build.rs b/build.rs index c1fc3ce..75b8608 100644 --- a/build.rs +++ b/build.rs @@ -3,13 +3,13 @@ use std::fs; // getdir will search for library with given pattern in nix store fn getdir(pattern: &str) -> Option { const DIR: &str = "/nix/store"; - + let entries = fs::read_dir(DIR).ok()?; for entry in entries { let entry = entry.ok()?; let entry_path = entry.path(); - + if !entry_path.is_dir() || entry_path.file_name() == None { continue; } @@ -31,7 +31,10 @@ fn main() { println!("cargo:rustc-link-search=native={}/lib", dir) } if let Some(dir) = getdir("apple-framework-CoreFoundation") { - println!("cargo:rustc-link-search=framework={}/Library/Frameworks/", dir) + println!( + "cargo:rustc-link-search=framework={}/Library/Frameworks/", + dir + ) } } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 72c291e..eac54e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,14 +76,14 @@ struct Args { nodes: Vec, /// Threshold - #[arg(short, long, num_args = 1, default_value_t = 3)] + #[arg(short, long, num_args = 1, default_value_t = 5)] threshold: u8, } +const DEBUG: bool = false; + #[main] async fn main() { - const DEBUG: bool = true; - let args = Args::parse(); // Init peripheral @@ -138,10 +138,12 @@ async fn main() { log_info!(logger, "Invoking query"); + let chanid = 869059488412139521u64; + let nodeid1 = (*resolver) .get_node( (*resolver) - .get_endpoints_async(869059488412139521u64) + .get_endpoints_async(chanid) .await .expect("channel data") .nodes[0], @@ -151,7 +153,7 @@ async fn main() { let nodeid2 = (*resolver) .get_node( (*resolver) - .get_endpoints_async(869059488412139521u64) + .get_endpoints_async(chanid) .await .expect("channel data") .nodes[1], @@ -162,7 +164,7 @@ async fn main() { logger, "{} --{}--> {}", nodeid1, - 869059488412139521u64, + chanid, nodeid2 ); }; diff --git a/src/resolve.rs b/src/resolve.rs index 22d7ba2..a59f9a8 100644 --- a/src/resolve.rs +++ b/src/resolve.rs @@ -36,6 +36,7 @@ use parking_lot::lock_api::RawMutex; const MAX_TIMEOUT: Duration = Duration::from_secs(128); const POLL_INTERVAL: Duration = Duration::from_secs(10); +const DEBUG: bool = false; pub type ResolvePeerManager = PeerManager< SocketDescriptor, @@ -323,6 +324,9 @@ where msg: lightning::ln::msgs::ReplyShortChannelIdsEnd, ) -> Result<(), lightning::ln::msgs::LightningError> { // Unlock the raw mutex + if DEBUG { + log_debug!(self.logger, "Short channel ids reply"); + } unsafe { if self.server_lock.is_locked() { self.server_lock.unlock(); diff --git a/src/voter.rs b/src/voter.rs index 6148dfc..15ca2c2 100644 --- a/src/voter.rs +++ b/src/voter.rs @@ -1,4 +1,4 @@ -use lightning::routing::gossip::NodeId; +use lightning::routing::gossip::{NodeId, NodeAlias}; use lightning::util::logger::Logger; use lightning::*; use std::collections::{HashMap, HashSet}; @@ -10,6 +10,11 @@ use super::resolve::*; /// Voter is able to count votes regarding unavailability +pub struct NodeData { + pub node_id: NodeId, + pub alias: NodeAlias, +} + pub struct Voter where L::Target: Logger, @@ -36,16 +41,36 @@ where *(self.resolver.lock().unwrap()) = Some(resolver.clone()); } + async fn get_node(&self, chanid: u64, direction: usize) -> NodeData { + let res = self.resolver.lock().unwrap().clone().unwrap(); + + let id = res.get_endpoints_async(chanid).await.expect("channel data").nodes[direction]; + + let n = res.get_node(id); + + if n == None { + log_trace!(self.logger, "Node data for nodeid {} not available", id); + + let result: [u8; 32] = [0; 32]; + + return NodeData { + node_id: id, + alias: NodeAlias(result), + }; + } + + let node = n.unwrap(); + + NodeData { + node_id: node.node_id, + alias: node.alias, + } + + } + pub async fn disable(&self, chanid: u64, direction: usize) { let res = self.resolver.lock().unwrap().clone().unwrap(); - let node = res - .get_node( - res.get_endpoints_async(chanid) - .await - .expect("channel data") - .nodes[direction], - ) - .expect("node data"); + let node = self.get_node(chanid, direction).await; log_trace!( self.logger, @@ -81,21 +106,16 @@ where } pub async fn enable(&self, chanid: u64, direction: usize) { - let res = self.resolver.lock().unwrap().clone().unwrap(); + { + let res = self.resolver.lock().unwrap().clone().unwrap(); - if !res.is_endpoint_cached(chanid) { - // Ignore enabling channels which we are unaware of - return; + if !res.is_endpoint_cached(chanid) { + // Ignore enabling channels which we are unaware of + return; + } } - let node = res - .get_node( - res.get_endpoints_async(chanid) - .await - .expect("channel data") - .nodes[direction], - ) - .expect("node data"); + let node = self.get_node(chanid, direction).await; log_trace!( self.logger,