Skip to content

Commit fcd9721

Browse files
committed
fix(routing): filter own-origin routes in announce — prevent redundant forged-origin rejections
1 parent 2b4d95e commit fcd9721

3 files changed

Lines changed: 66 additions & 4 deletions

File tree

internal/core/node/routing_integration.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ func (s *Service) handleAnnounceRoutes(senderIdentity domain.PeerIdentity, frame
352352
// syncSeqCounterLocked, breaking the per-origin SeqNo invariant.
353353
if wireRoute.Origin == s.identity.Address {
354354
rejected++
355-
log.Warn().
355+
log.Debug().
356356
Str("identity", wireRoute.Identity).
357357
Str("origin", wireRoute.Origin).
358358
Str("from", string(senderIdentity)).

internal/core/routing/table.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,10 @@ func (t *Table) RefreshDirectPeers() int {
677677

678678
// Announceable returns routes suitable for announcing to a specific peer,
679679
// applying split horizon: routes learned from excludeVia are omitted.
680+
// Routes originated by the peer (Origin == excludeVia) are also omitted —
681+
// the peer already knows its own routes and re-sending them wastes
682+
// bandwidth and triggers spurious "forged own origin" rejections on the
683+
// receiver side.
680684
// Withdrawn and expired routes are also excluded.
681685
//
682686
// Split horizon rule: routes where NextHop == excludeVia are not included
@@ -694,6 +698,9 @@ func (t *Table) Announceable(excludeVia PeerIdentity) []RouteEntry {
694698
if r.NextHop == excludeVia {
695699
continue
696700
}
701+
if r.Origin == excludeVia {
702+
continue
703+
}
697704
if r.IsWithdrawn() || r.IsExpired(now) {
698705
continue
699706
}
@@ -704,9 +711,13 @@ func (t *Table) Announceable(excludeVia PeerIdentity) []RouteEntry {
704711
}
705712

706713
// AnnounceTo returns the wire-safe projection of routes to announce to
707-
// a specific peer, applying split horizon and the +1 hop rule. This is
708-
// the preferred method for building announce_routes frames — it ensures
709-
// the boundary between model and wire format stays in the routing package.
714+
// a specific peer, applying split horizon, origin filtering and the +1
715+
// hop rule. This is the preferred method for building announce_routes
716+
// frames — it ensures the boundary between model and wire format stays
717+
// in the routing package.
718+
//
719+
// Origin filtering: routes where Origin == excludeVia are skipped because
720+
// the peer originated them and would reject them as forged own-origin.
710721
func (t *Table) AnnounceTo(excludeVia PeerIdentity) []AnnounceEntry {
711722
t.mu.RLock()
712723
defer t.mu.RUnlock()
@@ -719,6 +730,9 @@ func (t *Table) AnnounceTo(excludeVia PeerIdentity) []AnnounceEntry {
719730
if r.NextHop == excludeVia {
720731
continue
721732
}
733+
if r.Origin == excludeVia {
734+
continue
735+
}
722736
if r.IsWithdrawn() || r.IsExpired(now) {
723737
continue
724738
}

internal/core/routing/table_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,54 @@ func TestAnnounceToAppliesSplitHorizon(t *testing.T) {
497497
}
498498
}
499499

500+
// --- Origin filtering (don't send peer its own originated routes) ---
501+
502+
func TestAnnounceableOmitsRoutesOriginatedByPeer(t *testing.T) {
503+
now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
504+
tbl := NewTable(WithClock(fixedClock(now)))
505+
506+
// Route originated by peer-A, learned via peer-C (different NextHop).
507+
// Split horizon alone would NOT filter this, but origin filter must.
508+
mustUpdate(t, tbl, RouteEntry{
509+
Identity: "alice", Origin: "peer-A", NextHop: "peer-C",
510+
Hops: 3, SeqNo: 1, Source: RouteSourceAnnouncement,
511+
})
512+
mustUpdate(t, tbl, RouteEntry{
513+
Identity: "bob", Origin: "peer-B", NextHop: "peer-C",
514+
Hops: 2, SeqNo: 1, Source: RouteSourceAnnouncement,
515+
})
516+
517+
announceable := tbl.Announceable("peer-A")
518+
for _, r := range announceable {
519+
if r.Origin == "peer-A" {
520+
t.Fatal("origin filter: routes originated by peer-A must be excluded when announcing to peer-A")
521+
}
522+
}
523+
if len(announceable) != 1 || announceable[0].Identity != "bob" {
524+
t.Fatalf("expected only bob's route, got %+v", announceable)
525+
}
526+
}
527+
528+
func TestAnnounceToOmitsRoutesOriginatedByPeer(t *testing.T) {
529+
now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
530+
tbl := NewTable(WithClock(fixedClock(now)))
531+
532+
// Route originated by peer-A, learned via peer-C.
533+
mustUpdate(t, tbl, RouteEntry{
534+
Identity: "alice", Origin: "peer-A", NextHop: "peer-C",
535+
Hops: 3, SeqNo: 1, Source: RouteSourceAnnouncement,
536+
})
537+
mustUpdate(t, tbl, RouteEntry{
538+
Identity: "bob", Origin: "peer-B", NextHop: "peer-C",
539+
Hops: 2, SeqNo: 1, Source: RouteSourceAnnouncement,
540+
})
541+
542+
entries := tbl.AnnounceTo("peer-A")
543+
if len(entries) != 1 || entries[0].Identity != "bob" {
544+
t.Fatalf("origin filter should exclude alice's route: got %+v", entries)
545+
}
546+
}
547+
500548
func TestAnnounceToStripsInternalFields(t *testing.T) {
501549
now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
502550
tbl := NewTable(WithClock(fixedClock(now)))

0 commit comments

Comments
 (0)