From 69468c3219b579bb646cb0bf67c4af92a651b626 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 6 Apr 2026 18:40:23 -0500 Subject: [PATCH] lncfg+config: add tunable onion message rate limit options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit we surface the onion message rate limiter thresholds as ProtocolOptions so that operators can tune them from lnd.conf or the command line. Four options are added — onion-msg-peer-rate, onion-msg-peer-burst, onion-msg-global-rate, and onion-msg-global-burst — and are documented such that a rate of zero disables the corresponding limiter entirely. The default values are seeded from the constants added in the previous commit via DefaultConfig, following the same pattern that the Gossip sub-config already uses for its own rate limiter knobs. The fields are duplicated into protocol_integration.go so that the integration build tag sees the same surface; this mirrors how the existing NoOnionMessagesOption and related fields are declared. --- config.go | 70 ++++++++++++++++++++++++++ config_onion_ratelimit_test.go | 90 ++++++++++++++++++++++++++++++++++ lncfg/protocol.go | 23 +++++++++ lncfg/protocol_integration.go | 23 +++++++++ 4 files changed, 206 insertions(+) create mode 100644 config_onion_ratelimit_test.go diff --git a/config.go b/config.go index aaf748bc0..e3d8850e5 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "math" "net" "os" "os/user" @@ -41,6 +42,7 @@ import ( "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/onionmessage" "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/tor" @@ -583,6 +585,43 @@ type GRPCConfig struct { ClientAllowPingWithoutStream bool `long:"client-allow-ping-without-stream" description:"If true, the server allows keepalive pings from the client even when there are no active gRPC streams. This might be useful to keep the underlying HTTP/2 connection open for future requests."` } +// maxOnionMsgWireSize is the largest on-the-wire size in bytes, including +// the 2-byte message type prefix, that an OnionMessage can take. This is +// the value the rate limiter charges via OnionMessage.WireSize() for a +// max-sized message and therefore the tightest meaningful lower bound on +// the configured burst: anything smaller would reject every max-sized +// message even though the configured rate is positive. +const maxOnionMsgWireSize = 2 + lnwire.MaxMsgBody + +// validateOnionMsgLimiter validates a single onion message rate limiter +// kbps/burst-bytes pair. Both zero means "disabled"; both strictly positive +// means "enabled"; a mismatched pair is rejected so that operator typos +// surface at startup instead of silently disabling the limiter via the +// constructor fallback path. When enabled, burst-bytes must also be at +// least maxOnionMsgWireSize so that a single max-sized onion message +// (lnwire.MaxMsgBody bytes of body plus the 2-byte message-type prefix +// that WireSize charges for) can always fit in the token bucket; +// otherwise rate.Limiter.AllowN would reject every call and silently +// disable onion message forwarding. +func validateOnionMsgLimiter(name string, kbps, burstBytes uint64) error { + if (kbps > 0) != (burstBytes > 0) { + return fmt.Errorf("%s kbps and burst-bytes must both be "+ + "positive or both be zero; got kbps=%v "+ + "burst-bytes=%v", name, kbps, burstBytes) + } + if burstBytes > 0 && burstBytes < maxOnionMsgWireSize { + return fmt.Errorf("%s burst-bytes=%v must be at least %v "+ + "so a single max-sized onion message can fit in "+ + "the bucket", name, burstBytes, maxOnionMsgWireSize) + } + if burstBytes > uint64(math.MaxInt) { + return fmt.Errorf("%s burst-bytes=%v exceeds maximum %v", + name, burstBytes, math.MaxInt) + } + + return nil +} + // DefaultConfig returns all default values for the Config struct. // //nolint:ll @@ -727,6 +766,16 @@ func DefaultConfig() Config { Backoff: defaultLeaderCheckBackoff, }, }, + // Only the onion message rate limiter fields are explicitly + // initialized here; all other ProtocolOptions fields rely on + // Go zero values, which happen to be the historical defaults + // for those flags. + ProtocolOptions: &lncfg.ProtocolOptions{ + OnionMsgPeerKbps: onionmessage.DefaultPeerOnionMsgKbps, + OnionMsgPeerBurstBytes: onionmessage.DefaultPeerOnionMsgBurstBytes, + OnionMsgGlobalKbps: onionmessage.DefaultGlobalOnionMsgKbps, + OnionMsgGlobalBurstBytes: onionmessage.DefaultGlobalOnionMsgBurstBytes, + }, Gossip: &lncfg.Gossip{ MaxChannelUpdateBurst: discovery.DefaultMaxChannelUpdateBurst, ChannelUpdateInterval: discovery.DefaultChannelUpdateInterval, @@ -1076,6 +1125,27 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, return nil, mkErr("error validating autopilot: %v", err) } + // Validate the onion message rate limiter configuration. We reject + // the mismatched case where one of kbps/burst-bytes is strictly + // positive but the other is zero, which would silently disable the + // limiter and leave the operator unprotected. Both zero is fine and + // explicitly means "disabled"; both positive is fine and enables + // the limiter. + if err := validateOnionMsgLimiter( + "protocol.onion-msg-peer", + cfg.ProtocolOptions.OnionMsgPeerKbps, + cfg.ProtocolOptions.OnionMsgPeerBurstBytes, + ); err != nil { + return nil, mkErr("%s", err) + } + if err := validateOnionMsgLimiter( + "protocol.onion-msg-global", + cfg.ProtocolOptions.OnionMsgGlobalKbps, + cfg.ProtocolOptions.OnionMsgGlobalBurstBytes, + ); err != nil { + return nil, mkErr("%s", err) + } + // Ensure that --maxchansize is properly handled when set by user. // For non-Wumbo channels this limit remains 16777215 satoshis by default // as specified in BOLT-02. For wumbo channels this limit is 1,000,000,000. diff --git a/config_onion_ratelimit_test.go b/config_onion_ratelimit_test.go new file mode 100644 index 000000000..b97a75785 --- /dev/null +++ b/config_onion_ratelimit_test.go @@ -0,0 +1,90 @@ +package lnd + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// TestValidateOnionMsgLimiter exercises every branch of +// validateOnionMsgLimiter: the happy-path cases (both zero, both positive +// with adequate burst) and every rejection branch (mismatched pair and +// undersized burst). Startup config validation is the first line of +// defense against a typo silently disabling the limiter, so every branch +// is exercised explicitly. +func TestValidateOnionMsgLimiter(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + kbps uint64 + burstBytes uint64 + wantErr string + }{ + { + name: "both zero disables", + kbps: 0, + burstBytes: 0, + }, + { + name: "both positive enables", + kbps: 512, + burstBytes: 8 * 32 * 1024, + }, + { + name: "large values pass", + kbps: 1_000_000, + burstBytes: 1_000_000, + }, + { + name: "burst exactly at min allowed", + kbps: 1, + burstBytes: 2 + lnwire.MaxMsgBody, + }, + { + name: "burst one below min max-msg wire size " + + "rejected", + kbps: 1, + burstBytes: 1 + lnwire.MaxMsgBody, + wantErr: "must be at least 65535", + }, + { + name: "positive kbps zero burst rejected", + kbps: 512, + burstBytes: 0, + wantErr: "kbps and burst-bytes must both be " + + "positive or both be zero", + }, + { + name: "zero kbps positive burst rejected", + kbps: 0, + burstBytes: 65_536, + wantErr: "kbps and burst-bytes must both be " + + "positive or both be zero", + }, + { + name: "burst below maxOnionMsgWireSize " + + "rejected", + kbps: 512, + burstBytes: 1024, + wantErr: "burst-bytes=1024 must be at least " + + "65535", + }, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := validateOnionMsgLimiter( + "test", tc.kbps, tc.burstBytes, + ) + if tc.wantErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/lncfg/protocol.go b/lncfg/protocol.go index a300b70b4..67a440129 100644 --- a/lncfg/protocol.go +++ b/lncfg/protocol.go @@ -74,6 +74,29 @@ type ProtocolOptions struct { // NoOnionMessagesOption disables onion message forwarding. NoOnionMessagesOption bool `long:"no-onion-messages" description:"disable support for onion messaging"` + // OnionMsgPeerKbps is the maximum sustained onion message ingress + // bandwidth, in decimal kilobits per second (1 Kbps = 1000 bits/s), + // that will be accepted from any single peer. Setting this to zero, + // together with a zero burst, disables the per-peer onion message + // rate limiter. + OnionMsgPeerKbps uint64 `long:"onion-msg-peer-kbps" description:"max onion message ingress rate from a single peer, in decimal kilobits per second; set both this and onion-msg-peer-burst-bytes to 0 to disable the per-peer limiter"` + + // OnionMsgPeerBurstBytes is the token bucket depth, in bytes, used + // by the per-peer onion message rate limiter. A value of zero, + // paired with a zero rate, disables the per-peer limiter. + OnionMsgPeerBurstBytes uint64 `long:"onion-msg-peer-burst-bytes" description:"token bucket burst for the per-peer onion message limiter, in bytes; set both this and onion-msg-peer-kbps to 0 to disable the per-peer limiter"` + + // OnionMsgGlobalKbps is the maximum sustained onion message ingress + // bandwidth, in decimal kilobits per second, that will be accepted + // across all peers combined. Setting this to zero, together with a + // zero burst, disables the global onion message rate limiter. + OnionMsgGlobalKbps uint64 `long:"onion-msg-global-kbps" description:"max onion message ingress rate across all peers combined, in decimal kilobits per second; set both this and onion-msg-global-burst-bytes to 0 to disable the global limiter"` + + // OnionMsgGlobalBurstBytes is the token bucket depth, in bytes, used + // by the global onion message rate limiter. A value of zero, paired + // with a zero rate, disables the global limiter. + OnionMsgGlobalBurstBytes uint64 `long:"onion-msg-global-burst-bytes" description:"token bucket burst for the global onion message limiter, in bytes; set both this and onion-msg-global-kbps to 0 to disable the global limiter"` + // NoExperimentalAccountabilityOption disables experimental accountability. NoExperimentalAccountabilityOption bool `long:"no-experimental-accountability" description:"do not forward experimental accountability signals"` diff --git a/lncfg/protocol_integration.go b/lncfg/protocol_integration.go index 0961b9454..b28b03193 100644 --- a/lncfg/protocol_integration.go +++ b/lncfg/protocol_integration.go @@ -77,6 +77,29 @@ type ProtocolOptions struct { // NoOnionMessagesOption disables onion message forwarding. NoOnionMessagesOption bool `long:"no-onion-messages" description:"disable support for onion messaging"` + // OnionMsgPeerKbps is the maximum sustained onion message ingress + // bandwidth, in decimal kilobits per second (1 Kbps = 1000 bits/s), + // that will be accepted from any single peer. Setting this to zero, + // together with a zero burst, disables the per-peer onion message + // rate limiter. + OnionMsgPeerKbps uint64 `long:"onion-msg-peer-kbps" description:"max onion message ingress rate from a single peer, in decimal kilobits per second; set both this and onion-msg-peer-burst-bytes to 0 to disable the per-peer limiter"` + + // OnionMsgPeerBurstBytes is the token bucket depth, in bytes, used + // by the per-peer onion message rate limiter. A value of zero, + // paired with a zero rate, disables the per-peer limiter. + OnionMsgPeerBurstBytes uint64 `long:"onion-msg-peer-burst-bytes" description:"token bucket burst for the per-peer onion message limiter, in bytes; set both this and onion-msg-peer-kbps to 0 to disable the per-peer limiter"` + + // OnionMsgGlobalKbps is the maximum sustained onion message ingress + // bandwidth, in decimal kilobits per second, that will be accepted + // across all peers combined. Setting this to zero, together with a + // zero burst, disables the global onion message rate limiter. + OnionMsgGlobalKbps uint64 `long:"onion-msg-global-kbps" description:"max onion message ingress rate across all peers combined, in decimal kilobits per second; set both this and onion-msg-global-burst-bytes to 0 to disable the global limiter"` + + // OnionMsgGlobalBurstBytes is the token bucket depth, in bytes, used + // by the global onion message rate limiter. A value of zero, paired + // with a zero rate, disables the global limiter. + OnionMsgGlobalBurstBytes uint64 `long:"onion-msg-global-burst-bytes" description:"token bucket burst for the global onion message limiter, in bytes; set both this and onion-msg-global-kbps to 0 to disable the global limiter"` + // NoExperimentalAccountabilityOption disables experimental accountability. NoExperimentalAccountabilityOption bool `long:"no-experimental-accountability" description:"do not forward experimental accountability signals"`