lncfg+config: add tunable onion message rate limit options

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.
This commit is contained in:
Olaoluwa Osuntokun 2026-04-06 18:40:23 -05:00
parent d95bcbfa0b
commit 69468c3219
4 changed files with 206 additions and 0 deletions

View file

@ -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.

View file

@ -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)
})
}
}

View file

@ -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"`

View file

@ -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"`