mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-13 12:32:48 +02:00
onionmessage: drop onion messages cycling back to the sending peer
Block forwarding of an onion message when the resolved next hop is the
same peer that delivered it. Such a forward would immediately bounce the
message back over the very connection it arrived on, which is never
useful and can be abused to amplify traffic against a peer.
The check runs after the routing action is resolved, so both direct
next-node-ID and SCID-resolved paths are covered. A new
`ErrSamePeerCycle` is returned (and logged at warn level) when a cycle
is detected.
(cherry picked from commit 261babcf09)
This commit is contained in:
parent
ad841a7fb3
commit
405163961c
3 changed files with 130 additions and 0 deletions
|
|
@ -188,6 +188,32 @@ func (a *OnionPeerActor) Receive(ctx context.Context,
|
|||
return fn.Err[*Response](err)
|
||||
}
|
||||
|
||||
// Block same-peer cycles: do not forward a message back to
|
||||
// the peer that sent it.
|
||||
routingAction.WhenLeft(func(fwdAction forwardAction) {
|
||||
var nextNodeIDBytes [33]byte
|
||||
copy(
|
||||
nextNodeIDBytes[:],
|
||||
fwdAction.nextNodeID.SerializeCompressed(),
|
||||
)
|
||||
|
||||
if nextNodeIDBytes == a.peerPubKey {
|
||||
log.WarnS(logCtx,
|
||||
"Dropping cyclic onion message",
|
||||
ErrSamePeerCycle,
|
||||
lnutils.LogPubKey(
|
||||
"next_node_id",
|
||||
fwdAction.nextNodeID,
|
||||
),
|
||||
)
|
||||
|
||||
err = ErrSamePeerCycle
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fn.Err[*Response](err)
|
||||
}
|
||||
|
||||
// Handle the routing action.
|
||||
payload := fn.ElimEither(routingAction,
|
||||
func(fwdAction forwardAction) *lnwire.OnionMessagePayload {
|
||||
|
|
|
|||
|
|
@ -474,6 +474,105 @@ func TestOnionPeerActorRouting(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestOnionPeerActorSamePeerCycle verifies that the actor rejects onion
|
||||
// messages whose next hop is the same peer that sent them. Both the direct
|
||||
// next-node-ID and the SCID-resolved paths are covered.
|
||||
func TestOnionPeerActorSamePeerCycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type nextNodeFn func(h *actorHarness,
|
||||
pub *btcec.PublicKey) fn.Either[*btcec.PublicKey,
|
||||
lnwire.ShortChannelID]
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
nextNode nextNodeFn
|
||||
}{
|
||||
{
|
||||
name: "via next node ID",
|
||||
nextNode: func(_ *actorHarness,
|
||||
pub *btcec.PublicKey) fn.Either[
|
||||
*btcec.PublicKey, lnwire.ShortChannelID] {
|
||||
|
||||
return fn.NewLeft[*btcec.PublicKey,
|
||||
lnwire.ShortChannelID](pub)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "via SCID",
|
||||
nextNode: func(h *actorHarness,
|
||||
pub *btcec.PublicKey) fn.Either[
|
||||
*btcec.PublicKey, lnwire.ShortChannelID] {
|
||||
|
||||
scid := lnwire.NewShortChanIDFromInt(999)
|
||||
h.resolver.addPeer(scid, pub)
|
||||
|
||||
return fn.NewRight[*btcec.PublicKey](scid)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
h := newActorHarness(t)
|
||||
|
||||
// Generate a key for the next hop, then set the
|
||||
// actor's peerPubKey to the same key to simulate
|
||||
// the message arriving from that peer.
|
||||
nextNodeKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
nextNodePub := nextNodeKey.PubKey()
|
||||
|
||||
h.actor.peerPubKey = pubKeyToArray(nextNodePub)
|
||||
|
||||
nextNode := tc.nextNode(h, nextNodePub)
|
||||
rdA := record.NewNonFinalBlindedRouteDataOnionMessage(
|
||||
nextNode, nil, nil,
|
||||
)
|
||||
rdB := &record.BlindedRouteData{}
|
||||
|
||||
plainA := EncodeBlindedRouteData(t, rdA)
|
||||
plainB := EncodeBlindedRouteData(t, rdB)
|
||||
hops := []*sphinx.HopInfo{
|
||||
{
|
||||
NodePub: h.nodeKey.PubKey(),
|
||||
PlainText: plainA,
|
||||
},
|
||||
{NodePub: nextNodePub, PlainText: plainB},
|
||||
}
|
||||
|
||||
blindedPath := BuildBlindedPath(t, hops)
|
||||
onionMsg, _ := BuildOnionMessage(t, blindedPath, nil)
|
||||
|
||||
req := &Request{msg: *onionMsg}
|
||||
result := h.actor.Receive(t.Context(), req)
|
||||
|
||||
// The actor must return an error.
|
||||
require.True(t, result.IsErr())
|
||||
result.WhenErr(func(err error) {
|
||||
require.ErrorIs(t, err, ErrSamePeerCycle)
|
||||
})
|
||||
|
||||
// No message should have been forwarded.
|
||||
select {
|
||||
case <-h.sender.sent:
|
||||
require.FailNow(t, "message should not have "+
|
||||
"been forwarded back to the sending "+
|
||||
"peer")
|
||||
default:
|
||||
}
|
||||
|
||||
// No update should have been dispatched.
|
||||
select {
|
||||
case <-h.dispatcher.updates:
|
||||
require.FailNow(t, "update should not be "+
|
||||
"dispatched for a cyclic message")
|
||||
default:
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnionPeerActorReceiveContextCanceled tests that OnionPeerActor.Receive
|
||||
// returns an error when the context is canceled.
|
||||
func TestOnionPeerActorReceiveContextCanceled(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -14,4 +14,9 @@ var (
|
|||
// ErrSCIDEmpty is returned when the short channel ID is missing from
|
||||
// the route data.
|
||||
ErrSCIDEmpty = errors.New("short channel ID empty")
|
||||
|
||||
// ErrSamePeerCycle is returned when a forwarding onion message
|
||||
// would be sent back to the same peer it was received from.
|
||||
ErrSamePeerCycle = errors.New("onion message cycle: next " +
|
||||
"hop is the sending peer")
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue