Skip to content

Commit e8b6711

Browse files
committed
add ping and related options, methods and prepare release
1 parent f169603 commit e8b6711

File tree

14 files changed

+256
-40
lines changed

14 files changed

+256
-40
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# Changelog
22

3-
## [0.4.0] - Unreleased
3+
## [0.4.0] - 2024-10-20
44

55
### Added
6-
- more precise feedback if custom rule denies a peer, lists offending comparisons and their actual value
6+
- new custom rule variable ``ping``: check the time it takes to send a ``clnrod-pinglength`` bytes long message to the opening peer and back. Defaults to the average of 3 pings with 256 bytes length. Timeouts and errors will log but not flat out reject the channel, instead the timeout value of 5000 will be used. It is recommended to have email notifications on or watch the logs for ping timeouts (``Clnrod ping TIMEOUT``), since i encountered a rare case of CLN's ping getting stuck, requiring a node restart
7+
- new rpc method ``clnrod-testping`` *pubkey* [*count*] [*length*]: try the ping measurements with a few options
8+
- new option ``clnrod-pinglength``: set the length of the ping message for the custom rule check. Defaults to 256 bytes
9+
- more precise feedback if a custom rule rejects a peer, lists offending comparisons (non-exhaustive) that caused the rejection and their actual value
710

811
## [0.3.0] - 2024-09-23
912

Cargo.lock

Lines changed: 19 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "clnrod"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
edition = "2021"
55

66

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ New rpc methods with this plugin:
5454
* example: ``lightning-cli clnrod-testrule -k pubkey=02eadbd9e7557375161df8b646776a547c5cbc2e95b3071ec81553f8ec2cea3b8c public=true their_funding_sat=1000000 rule='amboss_terminal_web_rank < 1000'``
5555
* **clnrod-testmail**
5656
* send a test mail to check your email config
57+
* **clnrod-testping** *pubkey* [*count*] [*length*]
58+
* measure the time it takes in ms to send a *length* (Default: 256) bytes message to the node with *pubkey* and back. Pings *count* (Default: 3) times. You must connect to the node first!
59+
5760

5861
## Blockmode: allow
5962
Setting the blockmode to allow means:
@@ -87,6 +90,7 @@ The custom rule can make use of the following symbols:
8790
Variables starting with ``cln_`` query your own gossip, ``amboss_`` the [Amboss](https://amboss.space) API and ``oneml_`` the [1ML](https://1ml.com/) API. There is an one hour cache for collecting data that will be reset if you change the ``clnrod-customrule`` option.
8891
* ``their_funding_sat``: how much sats they are willing to open with on their side
8992
* ``public``: if the peer intends to open the channel as public this will be ``true`` otherwise ``false``
93+
* ``ping``: time it takes in ms to send a ``clnrod-pinglength`` (Default: 256) bytes packet to the opener and back. Timeouts and errors will log but not flat out reject the channel, instead the timeout value of 5000 will be used. It is recommended to have email notifications on or watch the logs for ping timeouts (``Clnrod ping TIMEOUT``), since i encountered a rare case of CLN's ping getting stuck, requiring a node restart
9094
* ``cln_node_capacity_sat``: the total capacity of the peer in sats
9195
* ``cln_channel_count``: the number of channels of the peer
9296
* ``cln_has_clearnet``: if the peer has any clearnet addresses published this will be ``true`` otherwise ``false``
@@ -127,6 +131,7 @@ You can mix two methods and if you set the same option with different methods, i
127131
* ``clnrod-denymessage``: The custom message we will send to a rejected peer, defaults to `CLNROD: Channel rejected by channel acceptor, sorry!`
128132
* ``clnrod-blockmode``: Set the preferred block mode to *allow* or *deny*, defaults to *deny* (with no config clnrod accepts all channels, see Documentation)
129133
* ``clnrod-customrule``: Set the custom rule for accepting channels, see Documentation, defaults to none
134+
* ``clnrod-pinglength``: Set the length of the ping message for the custom rule check. Defaults to `256` bytes
130135
### email
131136
* ``clnrod-smtp-username``: smtp username for email notifications
132137
* ``clnrod-smtp-password``: smtp password for email notifications

coffee.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
plugin:
22
name: clnrod
3-
version: 0.3.0
3+
version: 0.4.0
44
lang: rust
55
install: |
66
cargo build --release && cp target/release/clnrod . && cargo clean

src/collect.rs

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
use std::{
22
path::{Path, PathBuf},
33
sync::Arc,
4-
time::{Duration, SystemTime, UNIX_EPOCH},
4+
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
55
};
66

77
use anyhow::{anyhow, Error};
88
use cln_plugin::Plugin;
99
use cln_rpc::{
1010
model::{
11-
requests::{ListchannelsRequest, ListnodesRequest},
11+
requests::{GetinfoRequest, ListchannelsRequest, ListnodesRequest, PingRequest},
1212
responses::ListnodesNodesAddressesType,
1313
},
1414
primitives::{Amount, PublicKey},
1515
ClnRpc,
1616
};
17-
use log::debug;
17+
use log::{debug, info, warn};
1818
use serde_json::{json, Value};
19-
use tokio::time;
19+
use tokio::time::{self, timeout};
2020

21-
use crate::structs::{
22-
AmbossResponse, ChannelFlags, OneMl, PeerData, PeerDataCache, PeerInfo, PluginState,
21+
use crate::{
22+
notify::notify,
23+
structs::{
24+
AmbossResponse, ChannelFlags, NotifyVerbosity, OneMl, PeerData, PeerDataCache, PeerInfo,
25+
PluginState,
26+
},
2327
};
2428

2529
async fn get_oneml_data(
@@ -236,6 +240,7 @@ pub async fn collect_data(
236240
their_funding_msat: Amount,
237241
channel_flags: ChannelFlags,
238242
custom_rule: &str,
243+
ping_length: u16,
239244
) -> Result<PeerData, Error> {
240245
debug!("collect_data: start");
241246
let unix_now_s = SystemTime::now()
@@ -254,6 +259,15 @@ pub async fn collect_data(
254259
let network = plugin.configuration().network;
255260

256261
debug!("collect_data: custom_rule: {}", custom_rule);
262+
let ping_task = if custom_rule.to_ascii_lowercase().contains("ping") {
263+
let plugin_ping = plugin.clone();
264+
Some(tokio::spawn(async move {
265+
ln_ping(plugin_ping, pubkey, 3, ping_length).await
266+
}))
267+
} else {
268+
None
269+
};
270+
257271
let gossip_task = if custom_rule.to_ascii_lowercase().contains("cln_") {
258272
let channel_flags_clone = channel_flags.clone();
259273
Some(tokio::spawn(async move {
@@ -299,6 +313,13 @@ pub async fn collect_data(
299313
None
300314
};
301315

316+
let ping = if let Some(p) = ping_task {
317+
let pings = p.await??;
318+
Some((pings.iter().map(|y| *y as usize).sum::<usize>() / pings.len()) as u16)
319+
} else {
320+
None
321+
};
322+
302323
let peerinfo = if let Some(gdata) = gossip_task {
303324
gdata.await??
304325
} else {
@@ -330,6 +351,7 @@ pub async fn collect_data(
330351
debug!("collect_data: oneml_data: {:?}", oneml_data);
331352

332353
let peer_data = PeerData {
354+
ping,
333355
peerinfo,
334356
oneml_data,
335357
amboss_data,
@@ -376,3 +398,78 @@ fn check_feature(hex: &str, check_bits: Vec<u16>) -> Result<bool, Error> {
376398
}
377399
Ok(result)
378400
}
401+
402+
pub async fn ln_ping(
403+
plugin: Plugin<PluginState>,
404+
pubkey: PublicKey,
405+
count: u64,
406+
ping_length: u16,
407+
) -> Result<Vec<u16>, Error> {
408+
let timeout_ms = 5000;
409+
let rpc_path =
410+
Path::new(&plugin.configuration().lightning_dir).join(plugin.configuration().rpc_file);
411+
let mut rpc = ClnRpc::new(rpc_path).await?;
412+
let now_delay = Instant::now();
413+
let _dummy_rpc = rpc.call_typed(&GetinfoRequest {}).await;
414+
let rpc_delay = now_delay.elapsed().as_millis() as u16;
415+
info!(
416+
"Rpc delay that will be subtracted from ping: {}ms",
417+
rpc_delay
418+
);
419+
let mut results = Vec::new();
420+
let mut c = 0;
421+
while c < count {
422+
c += 1;
423+
let now = Instant::now();
424+
let timeout_result = match timeout(
425+
Duration::from_millis(timeout_ms as u64),
426+
rpc.call_typed(&PingRequest {
427+
len: Some(ping_length),
428+
pongbytes: Some(ping_length),
429+
id: pubkey,
430+
}),
431+
)
432+
.await
433+
{
434+
Ok(o) => o,
435+
Err(e) => {
436+
results.push(timeout_ms);
437+
notify(
438+
&plugin,
439+
"Clnrod ping TIMEOUT.",
440+
&format!(
441+
"Pinging {} {}/{} times with {} bytes TIMED OUT: {}\
442+
\n Please check if the `lightning-cli ping` command is stuck for your \
443+
node and requires a restart of CLN",
444+
pubkey, c, count, ping_length, e
445+
),
446+
Some(pubkey),
447+
NotifyVerbosity::Error,
448+
)
449+
.await;
450+
break;
451+
}
452+
};
453+
let ping_response = match timeout_result {
454+
Ok(o) => o,
455+
Err(e) => {
456+
results.push(timeout_ms);
457+
warn!("Ping error: {}", e);
458+
time::sleep(Duration::from_millis(250)).await;
459+
continue;
460+
}
461+
};
462+
if ping_response.totlen < ping_length {
463+
info!("Did not receive the full length ping back");
464+
}
465+
let ping = (now.elapsed().as_millis() as u16).saturating_sub(rpc_delay);
466+
info!(
467+
"Pinged {} {}/{} times with {} bytes in {}ms",
468+
pubkey, c, count, ping_length, ping
469+
);
470+
results.push(ping);
471+
time::sleep(Duration::from_millis(250)).await;
472+
}
473+
474+
Ok(results)
475+
}

src/config.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::{anyhow, Error};
1+
use anyhow::{anyhow, Context, Error};
22
use cln_plugin::{options, ConfiguredPlugin, Plugin};
33
use cln_rpc::primitives::PublicKey;
44
use cln_rpc::RpcError;
@@ -21,8 +21,8 @@ use crate::{
2121
};
2222
use crate::{
2323
OPT_BLOCK_MODE, OPT_CUSTOM_RULE, OPT_DENY_MESSAGE, OPT_EMAIL_FROM, OPT_EMAIL_TO,
24-
OPT_NOTIFY_VERBOSITY, OPT_SMTP_PASSWORD, OPT_SMTP_PORT, OPT_SMTP_SERVER, OPT_SMTP_USERNAME,
25-
PLUGIN_NAME,
24+
OPT_NOTIFY_VERBOSITY, OPT_PING_LENGTH, OPT_SMTP_PASSWORD, OPT_SMTP_PORT, OPT_SMTP_SERVER,
25+
OPT_SMTP_USERNAME, PLUGIN_NAME,
2626
};
2727

2828
pub async fn read_config(
@@ -100,6 +100,9 @@ fn get_startup_options(
100100
if let Some(cr) = plugin.option_str(OPT_CUSTOM_RULE)? {
101101
check_option(&mut config, OPT_CUSTOM_RULE, &cr)?;
102102
};
103+
if let Some(pl) = plugin.option_str(OPT_PING_LENGTH)? {
104+
check_option(&mut config, OPT_PING_LENGTH, &pl)?;
105+
};
103106
if let Some(smtp_user) = plugin.option_str(OPT_SMTP_USERNAME)? {
104107
check_option(&mut config, OPT_SMTP_USERNAME, &smtp_user)?;
105108
};
@@ -139,10 +142,21 @@ fn check_option(config: &mut Config, name: &str, value: &options::Value) -> Resu
139142
parse_rule(value.as_str().unwrap())?;
140143
config.custom_rule = value.as_str().unwrap().to_string();
141144
}
145+
n if n.eq(OPT_PING_LENGTH) => {
146+
let ping_length = u16::try_from(value.as_i64().unwrap())
147+
.context(format!("{} out of valid range", OPT_PING_LENGTH))?;
148+
if ping_length == 0 {
149+
return Err(anyhow!("{} must be greater than 0", OPT_PING_LENGTH));
150+
}
151+
config.ping_length = ping_length;
152+
}
142153
n if n.eq(OPT_SMTP_USERNAME) => config.smtp_username = value.as_str().unwrap().to_string(),
143154
n if n.eq(OPT_SMTP_PASSWORD) => config.smtp_password = value.as_str().unwrap().to_string(),
144155
n if n.eq(OPT_SMTP_SERVER) => config.smtp_server = value.as_str().unwrap().to_string(),
145-
n if n.eq(OPT_SMTP_PORT) => config.smtp_port = u16::try_from(value.as_i64().unwrap())?,
156+
n if n.eq(OPT_SMTP_PORT) => {
157+
config.smtp_port = u16::try_from(value.as_i64().unwrap())
158+
.context(format!("{} out of valid range", OPT_SMTP_PORT))?
159+
}
146160
n if n.eq(OPT_EMAIL_FROM) => config.email_from = value.as_str().unwrap().to_string(),
147161
n if n.eq(OPT_EMAIL_TO) => config.email_to = value.as_str().unwrap().to_string(),
148162
n if n.eq(OPT_NOTIFY_VERBOSITY) => {
@@ -179,7 +193,7 @@ fn parse_option(name: &str, value: &serde_json::Value) -> Result<options::Value,
179193
Err(anyhow!("{} is not a string!", OPT_CUSTOM_RULE))
180194
}
181195
}
182-
n if n.eq(OPT_SMTP_PORT) => {
196+
n if n.eq(OPT_SMTP_PORT) | n.eq(OPT_PING_LENGTH) => {
183197
if let Some(n_i64) = value.as_i64() {
184198
return Ok(options::Value::Integer(n_i64));
185199
} else if let Some(n_str) = value.as_str() {

0 commit comments

Comments
 (0)