multi: add sphinx router without replay protection
What changed, and why it matters
This commit adds a separate onion-message router that deliberately skips the usual replay-protection logging used for payment routing. The change is intentional and documented: onion messages are not payments, so replay protection is considered unnecessary. There is no direct evidence in the commit of a vulnerability being fixed; it looks like feature plumbing for onion messaging support.
Review whether onion-message processing needs any replay or flood mitigation at a higher layer (e.g., per-peer rate limits, proof-of-work, or application-level deduplication), since this commit intentionally removes persistent replay protection from the sphinx layer. Treat as a design choice rather than an immediate vulnerability unless further context shows abuse.
Security signals we found
New code path uses a no-op replay log for onion-message sphinx processing
Existing payment sphinx processor is renamed but otherwise unchanged
Onion messages are explicitly treated as non-payment traffic
No input validation, rate-limiting, or replay-mitigation logic is added in this diff
Evidence from the diff
The patch introduces a second sphinx router (sphinxOnionMsg) backed by sphinx.NewNoOpReplayLog() and wires it into peer/brontide.go as SphinxOnionMsg, while renaming the existing payment processor to SphinxPayment. It also starts/stops the new router and adds an OnionMessage case to messageSummary. The commit message explicitly states the router is initialized ‘without persistent replay protection logging’ because onion messages do not involve payment routing.
Changed components
server.gopeer/brontide.goonion message processing pathsphinx router initializationInspect captured patch +52 / −13
diff --git a/peer/brontide.go b/peer/brontide.go
index 0f4d6e3..7d5ec0d 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -19,6 +19,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2"
+ sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/aliasmgr"
"github.com/lightningnetwork/lnd/brontide"
"github.com/lightningnetwork/lnd/buffer"
@@ -301,9 +302,13 @@ type Config struct {
// the Brontide.
RoutingPolicy models.ForwardingPolicy
- // Sphinx is used when setting up ChannelLinks so they can decode sphinx
- // onion blobs.
- Sphinx *hop.OnionProcessor
+ // SphinxPayment is used when setting up ChannelLinks so they can decode
+ // sphinx onion blobs.
+ SphinxPayment *hop.OnionProcessor
+
+ // SphinxOnionMsg is the router used to decode sphinx onion blobs from
+ // an onion_message_packet.
+ SphinxOnionMsg *sphinx.Router
// WitnessBeacon is used when setting up ChannelLinks so they can add any
// preimages that they learn.
@@ -910,6 +915,9 @@ func (p *Brontide) Start() error {
return fmt.Errorf("unable to load channels: %w", err)
}
+ // The onion message endpoint is used to handle incoming onion messages
+ // **from** this peer. This uses the message multiplexer to route
+ // messages to the endpoint for further processing.
onionMessageEndpoint := onionmessage.NewOnionEndpoint(
p.cfg.OnionMessageServer,
)
@@ -1446,8 +1454,8 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
//nolint:ll
linkCfg := htlcswitch.ChannelLinkConfig{
Peer: p,
- DecodeHopIterators: p.cfg.Sphinx.DecodeHopIterators,
- ExtractErrorEncrypter: p.cfg.Sphinx.ExtractErrorEncrypter,
+ DecodeHopIterators: p.cfg.SphinxPayment.DecodeHopIterators,
+ ExtractErrorEncrypter: p.cfg.SphinxPayment.ExtractErrorEncrypter,
FetchLastChannelUpdate: p.cfg.FetchLastChanUpdate,
HodlMask: p.cfg.Hodl.Mask(),
Registry: p.cfg.Invoices,
@@ -2576,6 +2584,15 @@ func messageSummary(msg lnwire.Message) string {
time.Unix(int64(msg.FirstTimestamp), 0),
msg.TimestampRange)
+ case *lnwire.OnionMessage:
+ var pathKey []byte
+ if msg.PathKey != nil {
+ pathKey = msg.PathKey.SerializeCompressed()
+ }
+
+ return fmt.Sprintf("path_key=%x, onion_len=%v", pathKey,
+ len(msg.OnionBlob))
+
case *lnwire.Stfu:
return fmt.Sprintf("chan_id=%v, initiator=%v", msg.ChanID,
msg.Initiator)
diff --git a/server.go b/server.go
index ff91306..4f841aa 100644
--- a/server.go
+++ b/server.go
@@ -376,7 +376,9 @@ type server struct {
chainArb *contractcourt.ChainArbitrator
- sphinx *hop.OnionProcessor
+ sphinxPayment *hop.OnionProcessor
+
+ sphinxOnionMsg *sphinx.Router
towerClientMgr *wtclient.Manager
@@ -606,6 +608,12 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
)
sphinxRouter := sphinx.NewRouter(nodeKeyECDH, replayLog)
+ // Initialize the onion message sphinx router. This router doesn't need
+ // replay protection.
+ sphinxOnionMsg := sphinx.NewRouter(
+ nodeKeyECDH, sphinx.NewNoOpReplayLog(),
+ )
+
writeBufferPool := pool.NewWriteBuffer(
pool.DefaultWriteBufferGCInterval,
pool.DefaultWriteBufferExpiryInterval,
@@ -707,7 +715,8 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
// TODO(roasbeef): derive proper onion key based on rotation
// schedule
- sphinx: hop.NewOnionProcessor(sphinxRouter),
+ sphinxPayment: hop.NewOnionProcessor(sphinxRouter),
+ sphinxOnionMsg: sphinxOnionMsg,
torController: torController,
@@ -794,7 +803,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
},
FwdingLog: dbs.ChanStateDB.ForwardingLog(),
SwitchPackager: channeldb.NewSwitchPackager(),
- ExtractErrorEncrypter: s.sphinx.ExtractErrorEncrypter,
+ ExtractErrorEncrypter: s.sphinxPayment.ExtractErrorEncrypter,
FetchLastChannelUpdate: s.fetchLastChanUpdate(),
Notifier: s.cc.ChainNotifier,
HtlcNotifier: s.htlcNotifier,
@@ -1347,7 +1356,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
Registry: s.invoices,
NotifyClosedChannel: s.channelNotifier.NotifyClosedChannelEvent,
NotifyFullyResolvedChannel: s.channelNotifier.NotifyFullyResolvedChannelEvent,
- OnionProcessor: s.sphinx,
+ OnionProcessor: s.sphinxPayment,
PaymentsExpirationGracePeriod: cfg.PaymentsExpirationGracePeriod,
IsForwardedHTLC: s.htlcSwitch.IsForwardedHTLC,
Clock: clock.NewDefaultClock(),
@@ -2332,8 +2341,17 @@ func (s *server) Start(ctx context.Context) error {
return
}
- cleanup = cleanup.add(s.sphinx.Stop)
- if err := s.sphinx.Start(); err != nil {
+ cleanup = cleanup.add(s.sphinxPayment.Stop)
+ if err := s.sphinxPayment.Start(); err != nil {
+ startErr = err
+ return
+ }
+
+ cleanup = cleanup.add(func() error {
+ s.sphinxOnionMsg.Stop()
+ return nil
+ })
+ if err := s.sphinxOnionMsg.Start(); err != nil {
startErr = err
return
}
@@ -2605,6 +2623,9 @@ func (s *server) Stop() error {
// Stop dispatching blocks to other systems immediately.
s.blockbeatDispatcher.Stop()
+ // Shutdown the onion router for onion messaging.
+ s.sphinxOnionMsg.Stop()
+
// Shutdown the wallet, funding manager, and the rpc server.
if err := s.chanStatusMgr.Stop(); err != nil {
srvrLog.Warnf("failed to stop chanStatusMgr: %v", err)
@@ -2612,7 +2633,7 @@ func (s *server) Stop() error {
if err := s.htlcSwitch.Stop(); err != nil {
srvrLog.Warnf("failed to stop htlcSwitch: %v", err)
}
- if err := s.sphinx.Stop(); err != nil {
+ if err := s.sphinxPayment.Stop(); err != nil {
srvrLog.Warnf("failed to stop sphinx: %v", err)
}
if err := s.invoices.Stop(); err != nil {
@@ -4400,7 +4421,8 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
ChainNotifier: s.cc.ChainNotifier,
BestBlockView: s.cc.BestBlockTracker,
RoutingPolicy: s.cc.RoutingPolicy,
- Sphinx: s.sphinx,
+ SphinxPayment: s.sphinxPayment,
+ SphinxOnionMsg: s.sphinxOnionMsg,
WitnessBeacon: s.witnessBeacon,
Invoices: s.invoices,
ChannelNotifier: s.channelNotifier,
Why this scored 24/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.