htlcswitch+peer: set and read aux custom records
What changed, and why it matters
This commit adds a new optional plugin hook called AuxChannelNegotiator to LND. It lets external 'auxiliary' components attach custom data to Lightning protocol handshake messages (init and channel re-establishment) and read the peer's custom data. The change itself is infrastructure: it wires up the hook but does not contain the actual plugin logic. Any security risk depends entirely on what future external components do with this hook, so on its own it is best viewed as a new attack surface rather than a vulnerability.
Treat this as a new privileged extension point. Review the AuxChannelNegotiator interface contract and any implementations for proper validation of peer-supplied custom records, length limits, and safe error handling. Ensure that only trusted auxiliary components can be registered, since they can inject handshake data and observe re-establishment events.
Security signals we found
New optional wire-message hook processes peer-controlled custom records during protocol handshake
Custom records from init messages are passed to an external negotiator without visible validation in this diff
Merged custom records are sent in the init message, so a misbehaving negotiator could alter handshake contents
Channel re-establish notifications expose a sensitive protocol event to external components
No input sanitization, size limits, or error-handling details are visible in the changed code
Evidence from the diff
The patch introduces an AuxChannelNegotiator option in peer/brontide.go and htlcswitch/link.go, and passes it through from server.go. When present, the negotiator is invoked in three places: (1) Brontide.handleInitMsg calls ProcessInitRecords on the peer’s init custom records; (2) Brontide.sendInitMsg calls GetInitRecords and merges the returned custom records into the outgoing init message; (3) channelLink.syncChanStates calls ProcessReestablish after receiving a channel_reestablish message. The interface is optional (fn.Option) and no implementation is included in this diff. The code trusts the negotiator to validate and safely handle arbitrary peer-supplied custom records.
Changed components
peer/brontide.gohtlcswitch/link.goserver.goLightning protocol init message handlingChannel re-establishment handlingInspect captured patch +67 / −2
diff --git a/htlcswitch/link.go b/htlcswitch/link.go
index 2d1dd7d..4c81964 100644
--- a/htlcswitch/link.go
+++ b/htlcswitch/link.go
@@ -298,6 +298,11 @@ type ChannelLinkConfig struct {
// used to manage the bandwidth of the link.
AuxTrafficShaper fn.Option[AuxTrafficShaper]
+ // AuxChannelNegotiator is an optional interface that allows aux channel
+ // implementations to inject and process custom records over channel
+ // related wire messages.
+ AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator]
+
// QuiescenceTimeout is the max duration that the channel can be
// quiesced. Any dependent protocols (dynamic commitments, splicing,
// etc.) must finish their operations under this timeout value,
@@ -987,6 +992,22 @@ func (l *channelLink) syncChanStates(ctx context.Context) error {
// In any case, we'll then process their ChanSync message.
l.log.Info("received re-establishment message from remote side")
+ // If we have an AuxChannelNegotiator we notify any external
+ // component for this message. This serves as a notification
+ // that the reestablish message was received.
+ l.cfg.AuxChannelNegotiator.WhenSome(
+ func(acn lnwallet.AuxChannelNegotiator) {
+ fundingPoint := l.channel.ChannelPoint()
+ cid := lnwire.NewChanIDFromOutPoint(
+ fundingPoint,
+ )
+
+ acn.ProcessReestablish(
+ cid, l.cfg.Peer.PubKey(),
+ )
+ },
+ )
+
var (
openedCircuits []CircuitKey
closedCircuits []CircuitKey
diff --git a/peer/brontide.go b/peer/brontide.go
index 57e340f..1ea16d1 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -456,6 +456,11 @@ type Config struct {
// used to modify the way the co-op close transaction is constructed.
AuxChanCloser fn.Option[chancloser.AuxChanCloser]
+ // AuxChannelNegotiator is an optional interface that allows aux channel
+ // implementations to inject and process custom records over channel
+ // related wire messages.
+ AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator]
+
// ShouldFwdExpEndorsement is a closure that indicates whether
// experimental endorsement signals should be set.
ShouldFwdExpEndorsement func() bool
@@ -1454,8 +1459,9 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
ShouldFwdExpEndorsement: p.cfg.ShouldFwdExpEndorsement,
DisallowQuiescence: p.cfg.DisallowQuiescence ||
!p.remoteFeatures.HasFeature(lnwire.QuiescenceOptional),
- AuxTrafficShaper: p.cfg.AuxTrafficShaper,
- QuiescenceTimeout: p.cfg.QuiescenceTimeout,
+ AuxTrafficShaper: p.cfg.AuxTrafficShaper,
+ AuxChannelNegotiator: p.cfg.AuxChannelNegotiator,
+ QuiescenceTimeout: p.cfg.QuiescenceTimeout,
}
// Before adding our new link, purge the switch of any pending or live
@@ -4537,6 +4543,19 @@ func (p *Brontide) handleInitMsg(msg *lnwire.Init) error {
return fmt.Errorf("data loss protection required")
}
+ // If we have an AuxChannelNegotiator and the peer sent aux features,
+ // process them.
+ p.cfg.AuxChannelNegotiator.WhenSome(
+ func(acn lnwallet.AuxChannelNegotiator) {
+ err = acn.ProcessInitRecords(
+ p.cfg.PubKeyBytes, msg.CustomRecords.Copy(),
+ )
+ },
+ )
+ if err != nil {
+ return fmt.Errorf("could not process init records: %w", err)
+ }
+
return nil
}
@@ -4597,6 +4616,30 @@ func (p *Brontide) sendInitMsg(legacyChan bool) error {
features.RawFeatureVector,
)
+ var err error
+
+ // If we have an AuxChannelNegotiator, get custom feature bits to
+ // include in the init message.
+ p.cfg.AuxChannelNegotiator.WhenSome(
+ func(negotiator lnwallet.AuxChannelNegotiator) {
+ var auxRecords lnwire.CustomRecords
+ auxRecords, err = negotiator.GetInitRecords(
+ p.cfg.PubKeyBytes,
+ )
+ if err != nil {
+ p.log.Warnf("Failed to get aux init features: "+
+ "%v", err)
+ return
+ }
+
+ mergedRecs := msg.CustomRecords.MergedCopy(auxRecords)
+ msg.CustomRecords = mergedRecs
+ },
+ )
+ if err != nil {
+ return err
+ }
+
return p.writeMessage(msg)
}
diff --git a/server.go b/server.go
index 44be180..33b8fd7 100644
--- a/server.go
+++ b/server.go
@@ -4431,6 +4431,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
AuxChanCloser: s.implCfg.AuxChanCloser,
AuxResolver: s.implCfg.AuxContractResolver,
AuxTrafficShaper: s.implCfg.TrafficShaper,
+ AuxChannelNegotiator: s.implCfg.AuxChannelNegotiator,
ShouldFwdExpEndorsement: func() bool {
if s.cfg.ProtocolOptions.NoExperimentalEndorsement() {
return false
Why this scored 25/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.