lncfg+config: add tunable onion message rate limit options
What changed, and why it matters
This commit adds user-facing configuration options for rate-limiting onion messages in LND. It does not change any runtime behavior by itself; it only exposes knobs that operators can tune and adds startup validation to catch configuration mistakes. There is no vulnerability introduced or fixed in this diff.
No security action required; treat as a normal feature/configuration enhancement. Review the companion commit that introduced the underlying onionmessage rate-limiter constants and logic if assessing the full security posture of onion message handling.
Security signals we found
Adds startup validation to prevent operator typos from silently disabling onion message rate limiters
References prior rate-limiter constants in onionmessage package (not shown in diff)
Includes unit tests for validation branches
Evidence from the diff
The patch surfaces four onion-message rate-limiter parameters as ProtocolOptions (onion-msg-peer-kbps, onion-msg-peer-burst-bytes, onion-msg-global-kbps, onion-msg-global-burst-bytes), seeds their defaults from constants defined in a prior commit, duplicates the declarations into the integration build, and adds validateOnionMsgLimiter() plus ValidateConfig calls to reject mismatched or undersized rate/burst pairs at startup. It is a configuration plumbing and validation change, not a security fix or an exploitable bug.
Changed components
lnd config parsing and validationlncfg.ProtocolOptionsonion message rate limiter configuration surfaceInspect captured patch +206 / −0
diff --git a/config.go b/config.go
index aaf748b..e3d8850 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 0000000..b97a757
--- /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 a300b70..67a4401 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 0961b94..b28b031 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"`
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.