From 194eaec328b505e29874b1f40187dd6ca38e949a Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri <101164840+sangbida@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:18:28 +1030 Subject: [PATCH 1/4] connectd: add dev_uniform_padding to connectd_wire.csv This boolean allows lightningd and connectd to be on the same page on whether the uniform padding experimental flag is enabled. --- connectd/connectd_wire.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/connectd/connectd_wire.csv b/connectd/connectd_wire.csv index d5f6aee33b01..700e68296e23 100644 --- a/connectd/connectd_wire.csv +++ b/connectd/connectd_wire.csv @@ -29,6 +29,8 @@ msgdata,connectd_init,dev_no_reconnect,bool, msgdata,connectd_init,dev_fast_reconnect,bool, msgdata,connectd_init,dev_limit_connections_inflight,bool, msgdata,connectd_init,dev_keep_nagle,bool, +# Pad outgoing messages to uniform 1460-byte segments (traffic analysis defence) +msgdata,connectd_init,dev_uniform_padding,bool, # Connectd->master, here are the addresses I bound, can announce. msgtype,connectd_init_reply,2100 From a729eb8e5af1bff83fbd180d0aa706e2a40c4b29 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri <101164840+sangbida@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:03:12 +1030 Subject: [PATCH 2/4] lightningd: add dev-uniform-padding flag to lightningd --- connectd/connectd.c | 4 +++- connectd/connectd.h | 3 +++ lightningd/connect_control.c | 3 ++- lightningd/lightningd.c | 1 + lightningd/lightningd.h | 3 +++ lightningd/options.c | 4 ++++ 6 files changed, 16 insertions(+), 2 deletions(-) diff --git a/connectd/connectd.c b/connectd/connectd.c index ad3f11eca9cf..16a3c9e500ee 100644 --- a/connectd/connectd.c +++ b/connectd/connectd.c @@ -1692,7 +1692,8 @@ static void connect_init(struct daemon *daemon, const u8 *msg) &daemon->dev_no_reconnect, &daemon->dev_fast_reconnect, &dev_limit_connections_inflight, - &daemon->dev_keep_nagle)) { + &daemon->dev_keep_nagle, + &daemon->dev_uniform_padding)) { /* This is a helper which prints the type expected and the actual * message, then exits (it should never be called!). */ master_badmsg(WIRE_CONNECTD_INIT, msg); @@ -2569,6 +2570,7 @@ int main(int argc, char *argv[]) daemon->dev_exhausted_fds = false; daemon->dev_lightningd_is_slow = false; daemon->dev_keep_nagle = false; + daemon->dev_uniform_padding = false; /* We generally allow 1MB per second per peer, except for dev testing */ daemon->gossip_stream_limit = 1000000; daemon->incoming_stream_limit = 1000000; diff --git a/connectd/connectd.h b/connectd/connectd.h index 102d70500fb1..25d254839075 100644 --- a/connectd/connectd.h +++ b/connectd/connectd.h @@ -318,6 +318,9 @@ struct daemon { /* Allow localhost to be considered "public", only with --developer */ bool dev_allow_localhost; + /* Pad outgoing messages to uniform 1460-byte segments (traffic analysis defence) */ + bool dev_uniform_padding; + /* How much to gossip allow a peer every second (bytes) */ size_t gossip_stream_limit; diff --git a/lightningd/connect_control.c b/lightningd/connect_control.c index e95a04db9167..58b3b44684f0 100644 --- a/lightningd/connect_control.c +++ b/lightningd/connect_control.c @@ -727,7 +727,8 @@ int connectd_init(struct lightningd *ld) !ld->reconnect, ld->dev_fast_reconnect, ld->dev_limit_connections_inflight, - ld->dev_keep_nagle); + ld->dev_keep_nagle, + ld->dev_uniform_padding); subd_req(ld->connectd, ld->connectd, take(msg), -1, 0, connect_init_done, NULL); diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 0241b992da20..3a669e1b3102 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -150,6 +150,7 @@ static struct lightningd *new_lightningd(const tal_t *ctx) ld->dev_strict_forwarding = false; ld->dev_limit_connections_inflight = false; ld->dev_keep_nagle = false; + ld->dev_uniform_padding = false; /*~ We try to ensure enough fds for twice the number of channels * we start with. We have a developer option to change that factor diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h index 9edba38d09fa..3a00f9f3e4be 100644 --- a/lightningd/lightningd.h +++ b/lightningd/lightningd.h @@ -369,6 +369,9 @@ struct lightningd { /* Tell connectd we don't want TCP_NODELAY */ bool dev_keep_nagle; + /* Pad outgoing messages to uniform 1460-byte segments (traffic analysis defence) */ + bool dev_uniform_padding; + /* tor support */ struct wireaddr *proxyaddr; bool always_use_proxy; diff --git a/lightningd/options.c b/lightningd/options.c index d9ef8e445a0e..88148f3e6d12 100644 --- a/lightningd/options.c +++ b/lightningd/options.c @@ -952,6 +952,10 @@ static void dev_register_opts(struct lightningd *ld) opt_set_bool, &ld->dev_keep_nagle, "Tell connectd not to set TCP_NODELAY."); + clnopt_noarg("--dev-uniform-padding", OPT_DEV, + opt_set_bool, + &ld->dev_uniform_padding, + "Pad all outgoing peer messages to uniform 1460-byte segments"); /* This is handled directly in daemon_developer_mode(), so we ignore it here */ clnopt_noarg("--dev-debug-self", OPT_DEV, opt_ignore, From 23e33f7cd010f0c3d3dc4809b1e99e51090e2ea0 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri <101164840+sangbida@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:07:30 +1030 Subject: [PATCH 3/4] connectd: gate uniform message padding behind --dev-uniform-padding Uniform padding (sending all messages as 1460-byte chunks) breaks peers running LND-based nodes: LND disconnects on receiving a ping(num_pong_bytes=65535) with "pong bytes exceeded" instead of ignoring it as required by BOLT #1. Gate the feature behind --dev-uniform-padding so it is opt-in rather than forced on all connections. Nodes that only peer with CLN can enable it for the traffic analysis defence. Changelog-Changed: uniform message padding is now opt-in via the --dev-uniform-padding flag. --- connectd/multiplex.c | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/connectd/multiplex.c b/connectd/multiplex.c index c219ba8d77c2..8cfaa8852105 100644 --- a/connectd/multiplex.c +++ b/connectd/multiplex.c @@ -484,10 +484,16 @@ static bool have_empty_encrypted_queue(const struct peer *peer) /* (Continue) writing the encrypted_peer_out array */ static struct io_plan *write_encrypted_to_peer(struct peer *peer) { - assert(membuf_num_elems(&peer->encrypted_peer_out) >= UNIFORM_MESSAGE_SIZE); + size_t avail = membuf_num_elems(&peer->encrypted_peer_out); + /* With padding: always a full uniform-size chunk. + * Without: flush whatever we have (caller ensures non-zero). */ + size_t write_size = peer->daemon->dev_uniform_padding + ? UNIFORM_MESSAGE_SIZE : avail; + + assert(avail >= write_size && write_size > 0); return io_write_partial(peer->to_peer, membuf_elems(&peer->encrypted_peer_out), - UNIFORM_MESSAGE_SIZE, + write_size, &peer->encrypted_peer_out_sent, write_to_peer, peer); } @@ -1244,8 +1250,11 @@ static struct io_plan *write_to_peer(struct io_conn *peer_conn, /* Wait for them to wake us */ return msg_queue_wait(peer_conn, peer->peer_outq, write_to_peer, peer); } - /* OK, add padding. */ - pad_encrypted_queue(peer); + /* OK, add padding (only if --dev-uniform-padding enabled). */ + if (peer->daemon->dev_uniform_padding) + pad_encrypted_queue(peer); + else + break; } else { if (peer->draining_state == WRITING_TO_PEER) status_peer_debug(&peer->id, "draining, but sending %s.", @@ -1263,6 +1272,14 @@ static struct io_plan *write_to_peer(struct io_conn *peer_conn, } peer->nonurgent_flush_timer = tal_free(peer->nonurgent_flush_timer); + + /* With uniform padding the buffer is always a full UNIFORM_MESSAGE_SIZE. + * Without it, write whatever we have; if nothing, go back to waiting. */ + if (have_empty_encrypted_queue(peer)) { + io_wake(&peer->subds); + io_wake(&peer->peer_in); + return msg_queue_wait(peer_conn, peer->peer_outq, write_to_peer, peer); + } return write_encrypted_to_peer(peer); } From 81bad5b6202fd49ab5b4946e0570ef879a3e6c81 Mon Sep 17 00:00:00 2001 From: Sangbida Chaudhuri <101164840+sangbida@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:47:53 +1030 Subject: [PATCH 4/4] test: add flag to test_constant_packet_size --- tests/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index dece3e1a87d2..87ee2b66d8b0 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -4818,7 +4818,7 @@ def test_constant_packet_size(node_factory, tcp_capture): Test that TCP packets between nodes are constant size. This will be skipped unless you can run `dumpcap` (usually means you have to be in the `wireshark` group). """ - l1, l2, l3, l4 = node_factory.get_nodes(4) + l1, l2, l3, l4 = node_factory.get_nodes(4, opts={'dev-uniform-padding': None}) # Encrypted setup BOLT 8 has some short packets. l1.connect(l2)