diff --git a/config/peer.go b/config/peer.go index a192e317d3225560a7076232fac6d217ba4b790c..12124c7c6d3f44928dfaa5a1b9a05fa3bbf645a7 100644 --- a/config/peer.go +++ b/config/peer.go @@ -10,20 +10,22 @@ import ( // Peer defines the configuration for a BGP session type Peer struct { - AdminEnabled bool - ReconnectInterval time.Duration - KeepAlive time.Duration - HoldTime time.Duration - LocalAddress bnet.IP - PeerAddress bnet.IP - LocalAS uint32 - PeerAS uint32 - Passive bool - RouterID uint32 - AddPathSend routingtable.ClientOptions - AddPathRecv bool - ImportFilter *filter.Filter - ExportFilter *filter.Filter - RouteServerClient bool - IPv6 bool + AdminEnabled bool + ReconnectInterval time.Duration + KeepAlive time.Duration + HoldTime time.Duration + LocalAddress bnet.IP + PeerAddress bnet.IP + LocalAS uint32 + PeerAS uint32 + Passive bool + RouterID uint32 + AddPathSend routingtable.ClientOptions + AddPathRecv bool + ImportFilter *filter.Filter + ExportFilter *filter.Filter + RouteServerClient bool + RouteReflectorClient bool + RouteReflectorClusterID uint32 + IPv6 bool } diff --git a/protocols/bgp/packet/bgp.go b/protocols/bgp/packet/bgp.go index e008205a2ad95e97872c0a691a479efe42888c24..77b861110faf912a4ddbb0bb3510072b877a659c 100644 --- a/protocols/bgp/packet/bgp.go +++ b/protocols/bgp/packet/bgp.go @@ -14,6 +14,7 @@ const ( NLRIMaxLen = 5 CommunityLen = 4 LargeCommunityLen = 12 + ClusterIDLen = 4 OpenMsg = 1 UpdateMsg = 2 @@ -66,6 +67,8 @@ const ( AtomicAggrAttr = 6 AggregatorAttr = 7 CommunitiesAttr = 8 + OriginatorIDAttr = 9 + ClusterListAttr = 10 AS4PathAttr = 17 AS4AggregatorAttr = 18 LargeCommunitiesAttr = 32 diff --git a/protocols/bgp/packet/path_attributes.go b/protocols/bgp/packet/path_attributes.go index 77025f38ef976917f088509a8481aa3e53a363a5..27ebf80603af0fb91ab4e45adfa1223a0eabe72f 100644 --- a/protocols/bgp/packet/path_attributes.go +++ b/protocols/bgp/packet/path_attributes.go @@ -99,6 +99,14 @@ func decodePathAttr(buf *bytes.Buffer, opt *types.Options) (pa *PathAttribute, c if err := pa.decodeCommunities(buf); err != nil { return nil, consumed, fmt.Errorf("Failed to decode Community: %v", err) } + case OriginatorIDAttr: + if err := pa.decodeOriginatorID(buf); err != nil { + return nil, consumed, fmt.Errorf("Failed to decode OriginatorID: %v", err) + } + case ClusterListAttr: + if err := pa.decodeClusterList(buf); err != nil { + return nil, consumed, fmt.Errorf("Failed to decode OriginatorID: %v", err) + } case MultiProtocolReachNLRICode: if err := pa.decodeMultiProtocolReachNLRI(buf); err != nil { return nil, consumed, fmt.Errorf("Failed to multi protocol reachable NLRI: %v", err) @@ -364,6 +372,30 @@ func (pa *PathAttribute) decodeUint32(buf *bytes.Buffer, attrName string) error return nil } +func (pa *PathAttribute) decodeOriginatorID(buf *bytes.Buffer) error { + return pa.decodeUint32(buf, "OriginatorID") +} + +func (pa *PathAttribute) decodeClusterList(buf *bytes.Buffer) error { + if pa.Length%ClusterIDLen != 0 { + return fmt.Errorf("Unable to read ClusterList path attribute. Length %d is not divisible by %d", pa.Length, ClusterIDLen) + } + + count := pa.Length / ClusterIDLen + cids := make([]uint32, count) + + for i := uint16(0); i < count; i++ { + v, err := read4BytesAsUint32(buf) + if err != nil { + return err + } + cids[i] = v + } + + pa.Value = cids + return nil +} + func (pa *PathAttribute) setLength(buf *bytes.Buffer) (int, error) { bytesRead := 0 if pa.ExtendedLength { @@ -434,6 +466,10 @@ func (pa *PathAttribute) Serialize(buf *bytes.Buffer, opt *types.Options) uint16 pathAttrLen = uint16(pa.serializeCommunities(buf)) case LargeCommunitiesAttr: pathAttrLen = uint16(pa.serializeLargeCommunities(buf)) + case OriginatorIDAttr: + pathAttrLen = uint16(pa.serializeOriginatorID(buf)) + case ClusterListAttr: + pathAttrLen = uint16(pa.serializeClusterList(buf)) default: pathAttrLen = pa.serializeUnknownAttribute(buf) } @@ -595,6 +631,39 @@ func (pa *PathAttribute) serializeLargeCommunities(buf *bytes.Buffer) uint8 { return length } +func (pa *PathAttribute) serializeOriginatorID(buf *bytes.Buffer) uint8 { + attrFlags := uint8(0) + attrFlags = setOptional(attrFlags) + buf.WriteByte(attrFlags) + buf.WriteByte(OriginatorIDAttr) + length := uint8(4) + buf.WriteByte(length) + oid := pa.Value.(uint32) + buf.Write(convert.Uint32Byte(oid)) + return 7 +} + +func (pa *PathAttribute) serializeClusterList(buf *bytes.Buffer) uint8 { + cids := pa.Value.([]uint32) + if len(cids) == 0 { + return 0 + } + + attrFlags := uint8(0) + attrFlags = setOptional(attrFlags) + buf.WriteByte(attrFlags) + buf.WriteByte(ClusterListAttr) + + length := uint8(ClusterIDLen * len(cids)) + buf.WriteByte(length) + + for _, cid := range cids { + buf.Write(convert.Uint32Byte(cid)) + } + + return length +} + func (pa *PathAttribute) serializeUnknownAttribute(buf *bytes.Buffer) uint16 { attrFlags := uint8(0) if pa.Optional { @@ -664,7 +733,7 @@ func (pa *PathAttribute) AddOptionalPathAttributes(p *route.Path) *PathAttribute } // PathAttributes converts a path object into a linked list of path attributes -func PathAttributes(p *route.Path, iBGP bool) (*PathAttribute, error) { +func PathAttributes(p *route.Path, iBGP bool, rrClient bool) (*PathAttribute, error) { asPath := &PathAttribute{ TypeCode: ASPathAttr, Value: p.BGPPath.ASPath, @@ -711,6 +780,22 @@ func PathAttributes(p *route.Path, iBGP bool) (*PathAttribute, error) { last = localPref } + if rrClient { + originatorID := &PathAttribute{ + TypeCode: OriginatorIDAttr, + Value: p.BGPPath.OriginatorID, + } + last.Next = originatorID + last = originatorID + + clusterList := &PathAttribute{ + TypeCode: ClusterListAttr, + Value: p.BGPPath.ClusterList, + } + last.Next = clusterList + last = clusterList + } + optionals := last.AddOptionalPathAttributes(p) last = optionals diff --git a/protocols/bgp/packet/path_attributes_test.go b/protocols/bgp/packet/path_attributes_test.go index 3bd225984559e952a1a151c56834d15876a264ab..dd4b31e8005812f7a8f90c36b139d8ac244e320e 100644 --- a/protocols/bgp/packet/path_attributes_test.go +++ b/protocols/bgp/packet/path_attributes_test.go @@ -172,6 +172,80 @@ func TestDecodePathAttr(t *testing.T) { }, wantFail: true, }, + { + name: "Missing value OriginatorID", + input: []byte{ + 0, + OriginatorIDAttr, + 4, + }, + wantFail: true, + }, + { + name: "Valid OriginatorID", + input: []byte{ + 128, // Attr. Flags + OriginatorIDAttr, // Attr. Type Code + 4, // Attr. Length + 1, 2, 3, 4, + }, + wantFail: false, + expected: &PathAttribute{ + Length: 4, + Optional: true, + Transitive: false, + Partial: false, + ExtendedLength: false, + TypeCode: OriginatorIDAttr, + Value: bnet.IPv4FromOctets(1, 2, 3, 4).ToUint32(), + }, + }, + { + name: "one valid ClusterID", + input: []byte{ + 128, // Attr. Flags + ClusterListAttr, // Attr. Type Code + 4, // Attr. Length + 1, 1, 1, 1, + }, + wantFail: false, + expected: &PathAttribute{ + TypeCode: ClusterListAttr, + Optional: true, + Length: 4, + Value: []uint32{ + bnet.IPv4FromOctets(1, 1, 1, 1).ToUint32(), + }, + }, + }, + { + name: "two valid ClusterIDs", + input: []byte{ + setOptional(uint8(0)), // Attr. Flags + ClusterListAttr, // Attr. Type Code + 8, // Attr. Length + 1, 2, 3, 4, 8, 8, 8, 8, // 1.2.3.4, 8.8.8.8 + }, + wantFail: false, + expected: &PathAttribute{ + TypeCode: ClusterListAttr, + Optional: true, + Length: 8, + Value: []uint32{ + bnet.IPv4FromOctets(1, 2, 3, 4).ToUint32(), bnet.IPv4FromOctets(8, 8, 8, 8).ToUint32(), + }, + }, + }, + { + name: "one and a half ClusterID", + input: []byte{ + setOptional(uint8(0)), // Attr. Flags + ClusterListAttr, // Attr. Type Code + 8, // Attr. Length + 1, 2, 3, 4, 8, 8, // 1.2.3.4, 8.8. + }, + wantFail: true, + }, } for _, test := range tests { @@ -746,6 +820,85 @@ func TestDecodeCommunity(t *testing.T) { } } +func TestDecodeClusterList(t *testing.T) { + tests := []struct { + name string + input []byte + wantFail bool + explicitLength uint16 + expected *PathAttribute + }{ + { + name: "Empty input", + input: []byte{}, + wantFail: false, + expected: &PathAttribute{ + Length: 0, + Value: []uint32{}, + }, + }, + { + name: "one valid ClusterID", + input: []byte{ + 1, 1, 1, 1, + }, + wantFail: false, + expected: &PathAttribute{ + Length: 4, + Value: []uint32{ + bnet.IPv4FromOctets(1, 1, 1, 1).ToUint32(), + }, + }, + }, + { + name: "two valid ClusterIDs", + input: []byte{ + 1, 2, 3, 4, 8, 8, 8, 8, // 1.2.3.4, 8.8.8.8 + }, + wantFail: false, + expected: &PathAttribute{ + Length: 8, + Value: []uint32{ + bnet.IPv4FromOctets(1, 2, 3, 4).ToUint32(), bnet.IPv4FromOctets(8, 8, 8, 8).ToUint32(), + }, + }, + }, + { + name: "one and a half ClusterID", + input: []byte{ + 1, 2, 3, 4, 8, 8, // 1.2.3.4, 8.8. + }, + wantFail: true, + }, + } + + for _, test := range tests { + l := uint16(len(test.input)) + if test.explicitLength != 0 { + l = test.explicitLength + } + pa := &PathAttribute{ + Length: l, + } + err := pa.decodeClusterList(bytes.NewBuffer(test.input)) + + if test.wantFail { + if err != nil { + continue + } + t.Errorf("Expected error did not happen for test %q", test.name) + continue + } + + if err != nil { + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + assert.Equal(t, test.expected, pa) + } +} + func TestDecodeMultiProtocolReachNLRI(t *testing.T) { tests := []struct { name string @@ -1420,7 +1573,7 @@ func TestSerializeLargeCommunities(t *testing.T) { } for _, test := range tests { - t.Run(test.name, func(te *testing.T) { + t.Run(test.name, func(t *testing.T) { buf := bytes.NewBuffer([]byte{}) n := test.input.serializeLargeCommunities(buf) if n != test.expectedLen { @@ -1442,7 +1595,7 @@ func TestSerializeCommunities(t *testing.T) { { name: "2 communities", input: &PathAttribute{ - TypeCode: LargeCommunitiesAttr, + TypeCode: CommunitiesAttr, Value: []uint32{ 131080, 16778241, }, @@ -1467,7 +1620,7 @@ func TestSerializeCommunities(t *testing.T) { } for _, test := range tests { - t.Run(test.name, func(te *testing.T) { + t.Run(test.name, func(t *testing.T) { buf := bytes.NewBuffer([]byte{}) n := test.input.serializeCommunities(buf) if n != test.expectedLen { @@ -1479,6 +1632,106 @@ func TestSerializeCommunities(t *testing.T) { } } +func TestSerializeOriginatorID(t *testing.T) { + tests := []struct { + name string + input *PathAttribute + expected []byte + expectedLen uint8 + }{ + { + name: "Valid OriginatorID", + input: &PathAttribute{ + TypeCode: OriginatorIDAttr, + Value: bnet.IPv4FromOctets(1, 1, 1, 1).ToUint32(), + }, + expected: []byte{ + setOptional(uint8(0)), // Attribute flags + OriginatorIDAttr, // Type + 4, // Length + 1, 1, 1, 1, // ClusterID (1.1.1.1) + }, + expectedLen: 7, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + n := test.input.serializeOriginatorID(buf) + if n != test.expectedLen { + t.Errorf("Unexpected length for test %q: %d", test.name, n) + } + + assert.Equal(t, test.expected, buf.Bytes()) + }) + } +} + +func TestSerializeClusterList(t *testing.T) { + tests := []struct { + name string + input *PathAttribute + expected []byte + expectedLen uint8 + }{ + { + name: "Empty list of CluserIDs", + input: &PathAttribute{ + TypeCode: ClusterListAttr, + Value: []uint32{}, + }, + expected: []byte{}, + expectedLen: 0, + }, + { + name: "One ClusterID", + input: &PathAttribute{ + TypeCode: ClusterListAttr, + Value: []uint32{ + bnet.IPv4FromOctets(1, 1, 1, 1).ToUint32(), + }, + }, + expected: []byte{ + setOptional(uint8(0)), // Attribute flags + ClusterListAttr, // Type + 4, // Length + 1, 1, 1, 1, // ClusterID (1.1.1.1) + }, + expectedLen: 4, + }, + { + name: "Two ClusterIDs", + input: &PathAttribute{ + TypeCode: ClusterListAttr, + Value: []uint32{ + bnet.IPv4FromOctets(1, 1, 1, 1).ToUint32(), + bnet.IPv4FromOctets(192, 168, 23, 42).ToUint32(), + }, + }, + expected: []byte{ + setOptional(uint8(0)), // Attribute flags + ClusterListAttr, // Type + 8, // Length + 1, 1, 1, 1, 192, 168, 23, 42, // ClusterID (1.1.1.1) + }, + expectedLen: 8, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + n := test.input.serializeClusterList(buf) + if n != test.expectedLen { + t.Fatalf("Unexpected length for test %q: %d", test.name, n) + } + + assert.Equal(t, test.expected, buf.Bytes()) + }) + } +} + func TestSerializeUnknownAttribute(t *testing.T) { tests := []struct { name string @@ -1724,6 +1977,46 @@ func TestSerialize(t *testing.T) { 22, 185, 65, 240, // 185.65.240.0/22 }, }, + { + name: "Reflected NLRI", + msg: &BGPUpdate{ + NLRI: &NLRI{ + IP: strAddr("100.110.128.0"), + Pfxlen: 17, + }, + PathAttributes: &PathAttribute{ + TypeCode: OriginatorIDAttr, + Value: bnet.IPv4FromOctets(9, 8, 7, 6).ToUint32(), + Next: &PathAttribute{ + TypeCode: ClusterListAttr, + Value: []uint32{ + bnet.IPv4FromOctets(1, 2, 3, 4).ToUint32(), + }, + }, + }, + }, + expected: []byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0, 41, // Length + 2, // Msg Type + 0, 0, // Withdrawn Routes Length + + 0, 14, // Total Path Attribute Length + // OriginatorID + 128, // Attr. Flags (Opt.) + 9, // Attr. Type Code + 4, // Attr Length + 9, 8, 7, 6, // 9.8.7.6 + + // ClusterList + 128, // Attr Flags (Opt.) + 10, // Attr. Type Code + 4, + 1, 2, 3, 4, + + 17, 100, 110, 128, // NLRI + }, + }, } for _, test := range tests { diff --git a/protocols/bgp/server/fsm_established.go b/protocols/bgp/server/fsm_established.go index 3c5d96055663e167fc3e451ce03bed2340c757b7..644dde8f1e633465dbb73b4c9decf69a8fc7835a 100644 --- a/protocols/bgp/server/fsm_established.go +++ b/protocols/bgp/server/fsm_established.go @@ -59,7 +59,7 @@ func (s establishedState) run() (state, string) { func (s *establishedState) init() error { contributingASNs := s.fsm.rib.GetContributingASNs() - s.fsm.adjRIBIn = adjRIBIn.New(s.fsm.peer.importFilter, contributingASNs) + s.fsm.adjRIBIn = adjRIBIn.New(s.fsm.peer.importFilter, contributingASNs, s.fsm.peer.routerID, s.fsm.peer.clusterID) contributingASNs.Add(s.fsm.peer.localASN) s.fsm.adjRIBIn.Register(s.fsm.rib) @@ -77,13 +77,15 @@ func (s *establishedState) init() error { } n := &routingtable.Neighbor{ - Type: route.BGPPathType, - Address: s.fsm.peer.addr, - IBGP: s.fsm.peer.localASN == s.fsm.peer.peerASN, - LocalASN: s.fsm.peer.localASN, - RouteServerClient: s.fsm.peer.routeServerClient, - LocalAddress: localAddr, - CapAddPathRX: s.fsm.options.AddPathRX, + Type: route.BGPPathType, + Address: s.fsm.peer.addr, + IBGP: s.fsm.peer.localASN == s.fsm.peer.peerASN, + LocalASN: s.fsm.peer.localASN, + RouteServerClient: s.fsm.peer.routeServerClient, + LocalAddress: localAddr, + CapAddPathRX: s.fsm.options.AddPathRX, + RouteReflectorClient: s.fsm.peer.routeReflectorClient, + ClusterID: s.fsm.peer.clusterID, } s.fsm.adjRIBOut = adjRIBOut.New(n, s.fsm.peer.exportFilter) @@ -291,6 +293,10 @@ func (s *establishedState) processAttributes(attrs *packet.PathAttribute, path * path.BGPPath.Communities = pa.Value.([]uint32) case packet.LargeCommunitiesAttr: path.BGPPath.LargeCommunities = pa.Value.([]types.LargeCommunity) + case packet.OriginatorIDAttr: + path.BGPPath.OriginatorID = pa.Value.(uint32) + case packet.ClusterListAttr: + path.BGPPath.ClusterList = pa.Value.([]uint32) default: unknownAttr := s.processUnknownAttribute(pa) if unknownAttr != nil { diff --git a/protocols/bgp/server/fsm_test.go b/protocols/bgp/server/fsm_test.go index 2af938822e25b265831033c1b74fd690eef4e90d..5cca8d85ead35eeca7d49d04ab64e0cf8d6dc7ec 100644 --- a/protocols/bgp/server/fsm_test.go +++ b/protocols/bgp/server/fsm_test.go @@ -20,6 +20,7 @@ func TestFSM100Updates(t *testing.T) { rib: locRIB.New(), importFilter: filter.NewAcceptAllFilter(), exportFilter: filter.NewAcceptAllFilter(), + routerID: bnet.IPv4FromOctets(1, 1, 1, 1).ToUint32(), }) fsmA.holdTimer = time.NewTimer(time.Second * 90) diff --git a/protocols/bgp/server/peer.go b/protocols/bgp/server/peer.go index 638d8b0314c9b06fa8c65c7bcee24a117ee69388..02cf053bf000c5d1dd232c2243b7b617062a8fcf 100644 --- a/protocols/bgp/server/peer.go +++ b/protocols/bgp/server/peer.go @@ -28,17 +28,19 @@ type peer struct { fsms []*FSM fsmsMu sync.Mutex - rib *locRIB.LocRIB - routerID uint32 - addPathSend routingtable.ClientOptions - addPathRecv bool - reconnectInterval time.Duration - keepaliveTime time.Duration - holdTime time.Duration - optOpenParams []packet.OptParam - importFilter *filter.Filter - exportFilter *filter.Filter - routeServerClient bool + rib *locRIB.LocRIB + routerID uint32 + addPathSend routingtable.ClientOptions + addPathRecv bool + reconnectInterval time.Duration + keepaliveTime time.Duration + holdTime time.Duration + optOpenParams []packet.OptParam + importFilter *filter.Filter + exportFilter *filter.Filter + routeServerClient bool + routeReflectorClient bool + clusterID uint32 } func (p *peer) snapshot() PeerInfo { @@ -106,22 +108,30 @@ func newPeer(c config.Peer, rib *locRIB.LocRIB, server *bgpServer) (*peer, error c.LocalAS = server.localASN } p := &peer{ - server: server, - addr: c.PeerAddress, - peerASN: c.PeerAS, - localASN: c.LocalAS, - fsms: make([]*FSM, 0), - rib: rib, - addPathSend: c.AddPathSend, - addPathRecv: c.AddPathRecv, - reconnectInterval: c.ReconnectInterval, - keepaliveTime: c.KeepAlive, - holdTime: c.HoldTime, - optOpenParams: make([]packet.OptParam, 0), - importFilter: filterOrDefault(c.ImportFilter), - exportFilter: filterOrDefault(c.ExportFilter), - routeServerClient: c.RouteServerClient, + server: server, + addr: c.PeerAddress, + peerASN: c.PeerAS, + localASN: c.LocalAS, + fsms: make([]*FSM, 0), + rib: rib, + addPathSend: c.AddPathSend, + addPathRecv: c.AddPathRecv, + reconnectInterval: c.ReconnectInterval, + keepaliveTime: c.KeepAlive, + holdTime: c.HoldTime, + optOpenParams: make([]packet.OptParam, 0), + importFilter: filterOrDefault(c.ImportFilter), + exportFilter: filterOrDefault(c.ExportFilter), + routeServerClient: c.RouteServerClient, + routeReflectorClient: c.RouteReflectorClient, + clusterID: c.RouteReflectorClusterID, } + + // If we are a route reflector and no ClusterID was set, use our RouterID + if p.routeReflectorClient && p.clusterID == 0 { + p.clusterID = c.RouterID + } + p.fsms = append(p.fsms, NewActiveFSM2(p)) caps := make(packet.Capabilities, 0) diff --git a/protocols/bgp/server/update_sender.go b/protocols/bgp/server/update_sender.go index a223ee1752b63775a32a4715bbbb3232575a86ab..8d92ec0b683cf7b374366c6366bd4aeeefbee2ea 100644 --- a/protocols/bgp/server/update_sender.go +++ b/protocols/bgp/server/update_sender.go @@ -17,6 +17,7 @@ type UpdateSender struct { routingtable.ClientManager fsm *FSM iBGP bool + rrClient bool toSendMu sync.Mutex toSend map[string]*pathPfxs destroyCh chan struct{} @@ -31,6 +32,7 @@ func newUpdateSender(fsm *FSM) *UpdateSender { return &UpdateSender{ fsm: fsm, iBGP: fsm.peer.localASN == fsm.peer.peerASN, + rrClient: fsm.peer.routeReflectorClient, destroyCh: make(chan struct{}), toSend: make(map[string]*pathPfxs), } @@ -86,7 +88,7 @@ func (u *UpdateSender) sender(aggrTime time.Duration) { for key, pathNLRIs := range u.toSend { budget = packet.MaxLen - packet.HeaderLen - packet.MinUpdateLen - int(pathNLRIs.path.BGPPath.Length()) - pathAttrs, err = packet.PathAttributes(pathNLRIs.path, u.iBGP) + pathAttrs, err = packet.PathAttributes(pathNLRIs.path, u.iBGP, u.rrClient) if err != nil { log.Errorf("Unable to get path attributes: %v", err) continue diff --git a/route/bgp_path.go b/route/bgp_path.go index ee9aca8343129a52aebf5eac6cd487dc489cb466..a19813aa3158afc27a44a8f794bee600d49ebaf1 100644 --- a/route/bgp_path.go +++ b/route/bgp_path.go @@ -27,6 +27,8 @@ type BGPPath struct { Communities []uint32 LargeCommunities []types.LargeCommunity UnknownAttributes []types.UnknownPathAttribute + OriginatorID uint32 + ClusterList []uint32 } // Length get's the length of serialized path @@ -47,12 +49,22 @@ func (b *BGPPath) Length() uint16 { largeCommunitiesLen += 3 + uint16(len(b.LargeCommunities)*12) } + clusterListLen := uint16(0) + if len(b.ClusterList) != 0 { + clusterListLen += 3 + uint16(len(b.ClusterList)*4) + } + unknownAttributesLen := uint16(0) for _, unknownAttr := range b.UnknownAttributes { unknownAttributesLen += unknownAttr.WireLength() } - return communitiesLen + largeCommunitiesLen + 4*7 + 4 + asPathLen + unknownAttributesLen + originatorID := uint16(0) + if b.OriginatorID != 0 { + originatorID = 4 + } + + return communitiesLen + largeCommunitiesLen + 4*7 + 4 + originatorID + asPathLen + unknownAttributesLen } // ECMP determines if routes b and c are euqal in terms of ECMP @@ -110,12 +122,33 @@ func (b *BGPPath) Compare(c *BGPPath) int8 { // e) TODO: interiour cost (hello IS-IS and OSPF) - // f) - if c.BGPIdentifier < b.BGPIdentifier { + // f) + RFC4456 9. (Route Reflection) + bgpIdentifierC := c.BGPIdentifier + bgpIdentifierB := b.BGPIdentifier + + // IF an OriginatorID (set by an RR) is present, use this instead of Originator + if c.OriginatorID != 0 { + bgpIdentifierC = c.OriginatorID + } + + if b.OriginatorID != 0 { + bgpIdentifierB = b.OriginatorID + } + + if bgpIdentifierC < bgpIdentifierB { return 1 } - if c.BGPIdentifier > b.BGPIdentifier { + if bgpIdentifierC > bgpIdentifierB { + return -1 + } + + // Additionally check for the shorter ClusterList + if len(c.ClusterList) < len(b.ClusterList) { + return 1 + } + + if len(c.ClusterList) > len(b.ClusterList) { return -1 } @@ -219,6 +252,14 @@ func (b *BGPPath) Print() string { ret += fmt.Sprintf("\t\tCommunities: %v\n", b.Communities) ret += fmt.Sprintf("\t\tLargeCommunities: %v\n", b.LargeCommunities) + if b.OriginatorID != 0 { + oid := uint32To4Byte(b.OriginatorID) + ret += fmt.Sprintf("\t\tOriginatorID: %d.%d.%d.%d\n", oid[0], oid[1], oid[2], oid[3]) + } + if b.ClusterList != nil { + ret += fmt.Sprintf("\t\tClusterList %s\n", b.ClusterListString()) + } + return ret } @@ -280,6 +321,11 @@ func (b *BGPPath) Copy() *BGPPath { cp.LargeCommunities = make([]types.LargeCommunity, len(cp.LargeCommunities)) copy(cp.LargeCommunities, b.LargeCommunities) + if b.ClusterList != nil { + cp.ClusterList = make([]uint32, len(cp.ClusterList)) + copy(cp.ClusterList, b.ClusterList) + } + return &cp } @@ -295,6 +341,8 @@ func (b *BGPPath) ComputeHash() string { b.BGPIdentifier, b.Source, b.Communities, + b.OriginatorID, + b.ClusterList, b.LargeCommunities, b.PathIdentifier) @@ -311,6 +359,17 @@ func (b *BGPPath) CommunitiesString() string { return strings.TrimRight(str, " ") } +// ClusterListString returns the formated ClusterList +func (b *BGPPath) ClusterListString() string { + str := "" + for _, cid := range b.ClusterList { + octes := uint32To4Byte(cid) + str += fmt.Sprintf("%d.%d.%d.%d ", octes[0], octes[1], octes[2], octes[3]) + } + + return strings.TrimRight(str, " ") +} + // LargeCommunitiesString returns the formated communities func (b *BGPPath) LargeCommunitiesString() string { str := "" diff --git a/route/bgp_test.go b/route/bgp_test.go index f399d5d51ab96ad1a5b789a22aceb4d7b2490932..760306af6c0006479fd2c8c401581b57eff5ab48 100644 --- a/route/bgp_test.go +++ b/route/bgp_test.go @@ -36,9 +36,9 @@ func TestComputeHash(t *testing.T) { Source: bnet.IPv4(4), } - assert.Equal(t, "98d68e69d993f8807c561cc7d63de759f7edc732887f88a7ebf42f61b9e54821", p.ComputeHash()) + assert.Equal(t, "1058916ff3e6a51c7d8a47945d13fc3fcd8ee578a6d376505f46d58979b30fae", p.ComputeHash()) p.LocalPref = 150 - assert.NotEqual(t, "98d68e69d993f8807c561cc7d63de759f7edc732887f88a7ebf42f61b9e54821", p.ComputeHash()) + assert.NotEqual(t, "1058916ff3e6a51c7d8a47945d13fc3fcd8ee578a6d376505f46d58979b30fae", p.ComputeHash()) } diff --git a/routingtable/adjRIBIn/adj_rib_in.go b/routingtable/adjRIBIn/adj_rib_in.go index bf3bab1c8a4aa14d4f969429e4c10eae0aa74731..319e28690170ae37df83ec2c480cea752b6ce7f1 100644 --- a/routingtable/adjRIBIn/adj_rib_in.go +++ b/routingtable/adjRIBIn/adj_rib_in.go @@ -18,14 +18,18 @@ type AdjRIBIn struct { mu sync.RWMutex exportFilter *filter.Filter contributingASNs *routingtable.ContributingASNs + routerID uint32 + clusterID uint32 } // New creates a new Adjacency RIB In -func New(exportFilter *filter.Filter, contributingASNs *routingtable.ContributingASNs) *AdjRIBIn { +func New(exportFilter *filter.Filter, contributingASNs *routingtable.ContributingASNs, routerID uint32, clusterID uint32) *AdjRIBIn { a := &AdjRIBIn{ rt: routingtable.NewRoutingTable(), exportFilter: exportFilter, contributingASNs: contributingASNs, + routerID: routerID, + clusterID: clusterID, } a.ClientManager = routingtable.NewClientManager(a) return a @@ -64,6 +68,20 @@ func (a *AdjRIBIn) AddPath(pfx net.Prefix, p *route.Path) error { a.mu.Lock() defer a.mu.Unlock() + // RFC4456 Sect. 8: Ignore route with our RouterID as OriginatorID + if p.BGPPath.OriginatorID == a.routerID { + return nil + } + + // RFC4456 Sect. 8: Ignore routes which contian our ClusterID in their ClusterList + if len(p.BGPPath.ClusterList) > 0 { + for _, cid := range p.BGPPath.ClusterList { + if cid == a.clusterID { + return nil + } + } + } + oldPaths := a.rt.ReplacePath(pfx, p) a.removePathsFromClients(pfx, oldPaths) diff --git a/routingtable/adjRIBIn/adj_rib_in_test.go b/routingtable/adjRIBIn/adj_rib_in_test.go index 0fb052db2a5e34f26eb2132cf86cd03632f420c7..deec00104931a80f128be6782df70e35407775ae 100644 --- a/routingtable/adjRIBIn/adj_rib_in_test.go +++ b/routingtable/adjRIBIn/adj_rib_in_test.go @@ -12,6 +12,9 @@ import ( ) func TestAddPath(t *testing.T) { + routerID := net.IPv4FromOctets(1, 1, 1, 1).ToUint32() + clusterID := net.IPv4FromOctets(2, 2, 2, 2).ToUint32() + tests := []struct { name string routes []*route.Route @@ -72,10 +75,39 @@ func TestAddPath(t *testing.T) { }), }, }, + { + name: "Add route with our RouterID as OriginatorID", + routes: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 32), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + LocalPref: 111, + OriginatorID: routerID, + }, + }), + }, + expected: []*route.Route{}, + }, + { + name: "Add route with our ClusterID within ClusterList", + routes: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 32), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + LocalPref: 222, + OriginatorID: 23, + ClusterList: []uint32{ + clusterID, + }, + }, + }), + }, + expected: []*route.Route{}, + }, } for _, test := range tests { - adjRIBIn := New(filter.NewAcceptAllFilter(), routingtable.NewContributingASNs()) + adjRIBIn := New(filter.NewAcceptAllFilter(), routingtable.NewContributingASNs(), routerID, clusterID) mc := routingtable.NewRTMockClient() adjRIBIn.ClientManager.Register(mc) @@ -83,12 +115,14 @@ func TestAddPath(t *testing.T) { adjRIBIn.AddPath(route.Prefix(), route.Paths()[0]) } - removePathParams := mc.GetRemovePathParams() - if removePathParams.Pfx != test.removePfx { - t.Errorf("Test %q failed: Call to RemovePath did not propagate prefix properly: Got: %s Want: %s", test.name, removePathParams.Pfx.String(), test.removePfx.String()) - } + if test.removePath != nil { + removePathParams := mc.GetRemovePathParams() + if removePathParams.Pfx != test.removePfx { + t.Errorf("Test %q failed: Call to RemovePath did not propagate prefix properly: Got: %s Want: %s", test.name, removePathParams.Pfx.String(), test.removePfx.String()) + } - assert.Equal(t, test.removePath, removePathParams.Path) + assert.Equal(t, test.removePath, removePathParams.Path) + } assert.Equal(t, test.expected, adjRIBIn.rt.Dump()) } } @@ -167,7 +201,7 @@ func TestRemovePath(t *testing.T) { } for _, test := range tests { - adjRIBIn := New(filter.NewAcceptAllFilter(), routingtable.NewContributingASNs()) + adjRIBIn := New(filter.NewAcceptAllFilter(), routingtable.NewContributingASNs(), 1, 2) for _, route := range test.routes { adjRIBIn.AddPath(route.Prefix(), route.Paths()[0]) } diff --git a/routingtable/adjRIBOut/adj_rib_out.go b/routingtable/adjRIBOut/adj_rib_out.go index 2685ce37e45bd40fb91f891190a82e2054fee472..b4271ea3c82734d6dd8f5e8dae7f2fb48db34c4b 100644 --- a/routingtable/adjRIBOut/adj_rib_out.go +++ b/routingtable/adjRIBOut/adj_rib_out.go @@ -53,8 +53,8 @@ func (a *AdjRIBOut) AddPath(pfx bnet.Prefix, p *route.Path) error { return nil } - // Don't export routes learned via iBGP to an iBGP neighbor - if !p.BGPPath.EBGP && a.neighbor.IBGP { + // Don't export routes learned via iBGP to an iBGP neighbor which is NOT a route reflection client + if !p.BGPPath.EBGP && a.neighbor.IBGP && !a.neighbor.RouteReflectorClient { return nil } @@ -65,6 +65,27 @@ func (a *AdjRIBOut) AddPath(pfx bnet.Prefix, p *route.Path) error { p.BGPPath.NextHop = a.neighbor.LocalAddress } + // If the iBGP neighbor is a route reflection client... + if a.neighbor.IBGP && a.neighbor.RouteReflectorClient { + /* + * RFC4456 Section 8: + * This attribute will carry the BGP Identifier of the originator of the route in the local AS. + * A BGP speaker SHOULD NOT create an ORIGINATOR_ID attribute if one already exists. + */ + if p.BGPPath.OriginatorID == 0 { + p.BGPPath.OriginatorID = p.BGPPath.Source.ToUint32() + } + + /* + * When an RR reflects a route, it MUST prepend the local CLUSTER_ID to the CLUSTER_LIST. + * If the CLUSTER_LIST is empty, it MUST create a new one. + */ + cList := make([]uint32, len(p.BGPPath.ClusterList)+1) + copy(cList[1:], p.BGPPath.ClusterList) + cList[0] = a.neighbor.ClusterID + p.BGPPath.ClusterList = cList + } + p, reject := a.exportFilter.ProcessTerms(pfx, p) if reject { return nil diff --git a/routingtable/adjRIBOut/adj_rib_out_test.go b/routingtable/adjRIBOut/adj_rib_out_test.go index 0d01dd0890f06f1c518101205755162b10081c3e..26eceeeed34b37d62303a9156f9074ba28411129 100644 --- a/routingtable/adjRIBOut/adj_rib_out_test.go +++ b/routingtable/adjRIBOut/adj_rib_out_test.go @@ -498,7 +498,7 @@ func TestBestPathOnlyIBGP(t *testing.T) { expected: []*route.Route{}, }, { - name: "Try to add route with NO_EXPORT community set (without success)", + name: "Try to add route with NO_ADVERTISE community set (without success)", routesAdd: []*route.Route{ route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ Type: route.BGPPathType, @@ -532,6 +532,337 @@ func TestBestPathOnlyIBGP(t *testing.T) { } } +/* + * Test for iBGP Route Reflector client neighbor + */ + +func TestBestPathOnlyRRClient(t *testing.T) { + neighborBestOnlyRR := &routingtable.Neighbor{ + Type: route.BGPPathType, + LocalAddress: net.IPv4FromOctets(127, 0, 0, 1), + Address: net.IPv4FromOctets(127, 0, 0, 2), + IBGP: true, + LocalASN: 41981, + RouteServerClient: false, + CapAddPathRX: false, + RouteReflectorClient: true, + ClusterID: net.IPv4FromOctets(2, 2, 2, 2).ToUint32(), + } + + adjRIBOut := New(neighborBestOnlyRR, filter.NewAcceptAllFilter()) + + tests := []struct { + name string + routesAdd []*route.Route + routesRemove []*route.Route + expected []*route.Route + expectedCount int64 + }{ + { + name: "Add an iBGP route (with success)", + routesAdd: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{}, + }), + }, + expected: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + Communities: []uint32{}, + LargeCommunities: []types.LargeCommunity{}, + ASPath: types.ASPath{}, + ClusterList: []uint32{ + neighborBestOnlyRR.ClusterID, + }, + }, + }), + }, + expectedCount: 1, + }, + { + name: "Add an eBGP route (replacing previous iBGP route)", + routesAdd: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + EBGP: true, + ASPath: types.ASPath{ + types.ASPathSegment{ + Type: types.ASSequence, + ASNs: []uint32{ + 201701, + }, + }, + }, + ASPathLen: 1, + NextHop: net.IPv4FromOctets(1, 2, 3, 4), + }, + }), + }, + expected: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + NextHop: net.IPv4FromOctets(1, 2, 3, 4), + ASPath: types.ASPath{ + types.ASPathSegment{ + Type: types.ASSequence, + ASNs: []uint32{ + 201701, + }, + }, + }, + ASPathLen: 1, + Origin: 0, + MED: 0, + EBGP: true, + Communities: []uint32{}, + LargeCommunities: []types.LargeCommunity{}, + UnknownAttributes: nil, + PathIdentifier: 0, + LocalPref: 0, + Source: net.IP{}, + ClusterList: []uint32{ + neighborBestOnlyRR.ClusterID, + }, + }, + }), + }, + expectedCount: 1, + }, + { + name: "Try to remove slightly different route", + routesRemove: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + NextHop: net.IPv4FromOctets(1, 2, 3, 4), + ASPath: types.ASPath{ + types.ASPathSegment{ + Type: types.ASSequence, + ASNs: []uint32{ + 201701, + }, + }, + }, + ASPathLen: 1, + Origin: 0, + MED: 1, // Existing route has MED 0 + EBGP: true, + Communities: []uint32{}, + LargeCommunities: []types.LargeCommunity{}, + UnknownAttributes: nil, + PathIdentifier: 0, + LocalPref: 0, + Source: net.IP{}}, + }), + }, + expected: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + NextHop: net.IPv4FromOctets(1, 2, 3, 4), + ASPath: types.ASPath{ + types.ASPathSegment{ + Type: types.ASSequence, + ASNs: []uint32{ + 201701, + }, + }, + }, + ASPathLen: 1, + Origin: 0, + MED: 0, + EBGP: true, + Communities: []uint32{}, + LargeCommunities: []types.LargeCommunity{}, + UnknownAttributes: nil, + PathIdentifier: 0, + LocalPref: 0, + Source: net.IP{}, + ClusterList: []uint32{ + neighborBestOnlyRR.ClusterID, + }, + }, + }), + }, + expectedCount: 1, + }, + { + name: "Remove route added in 2nd step", + routesRemove: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + NextHop: net.IPv4FromOctets(1, 2, 3, 4), + ASPath: types.ASPath{ + types.ASPathSegment{ + Type: types.ASSequence, + ASNs: []uint32{ + 201701, + }, + }, + }, + ASPathLen: 1, + Origin: 0, + MED: 0, + EBGP: true, + Communities: []uint32{}, + LargeCommunities: []types.LargeCommunity{}, + UnknownAttributes: nil, + PathIdentifier: 0, + LocalPref: 0, + Source: net.IP{}, + ClusterList: []uint32{ + neighborBestOnlyRR.ClusterID, + }, + }, + }), + }, + expected: []*route.Route{}, + expectedCount: 0, + }, + { + name: "Try to add route with NO_ADVERTISE community set (without success)", + routesAdd: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + Communities: []uint32{ + types.WellKnownCommunityNoAdvertise, + }, + }, + }), + }, + expected: []*route.Route{}, + expectedCount: 0, + }, + { + name: "Try to add route with NO_EXPORT community set (with success)", + routesAdd: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + Communities: []uint32{ + types.WellKnownCommunityNoExport, + }, + }, + }), + }, + expected: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + ASPath: types.ASPath{}, + ASPathLen: 0, + Origin: 0, + MED: 0, + EBGP: false, + Communities: []uint32{ + types.WellKnownCommunityNoExport, + }, + LargeCommunities: []types.LargeCommunity{}, + UnknownAttributes: nil, + PathIdentifier: 0, + LocalPref: 0, + Source: net.IP{}, + ClusterList: []uint32{ + neighborBestOnlyRR.ClusterID, + }, + }, + }), + }, + expectedCount: 1, + }, + { + name: "Remove NO_EXPORT route added before", + routesRemove: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + ASPath: types.ASPath{}, + ASPathLen: 0, + Origin: 0, + MED: 0, + EBGP: false, + Communities: []uint32{ + types.WellKnownCommunityNoExport, + }, + LargeCommunities: []types.LargeCommunity{}, + UnknownAttributes: nil, + PathIdentifier: 0, + LocalPref: 0, + Source: net.IP{}, + ClusterList: []uint32{ + neighborBestOnlyRR.ClusterID, + }, + }, + }), + }, + expected: []*route.Route{}, + expectedCount: 0, + }, + { + name: "Add route with one entry in ClusterList and OriginatorID set (with success)", + routesAdd: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + OriginatorID: 42, + ClusterList: []uint32{ + 23, + }, + }, + }), + }, + expected: []*route.Route{ + route.NewRoute(net.NewPfx(net.IPv4FromOctets(10, 0, 0, 0), 8), &route.Path{ + Type: route.BGPPathType, + BGPPath: &route.BGPPath{ + ASPath: types.ASPath{}, + ASPathLen: 0, + Origin: 0, + MED: 0, + EBGP: false, + Communities: []uint32{}, + LargeCommunities: []types.LargeCommunity{}, + UnknownAttributes: nil, + PathIdentifier: 0, + LocalPref: 0, + Source: net.IP{}, + ClusterList: []uint32{ + neighborBestOnlyRR.ClusterID, + 23, + }, + OriginatorID: 42, + }, + }), + }, + expectedCount: 1, + }, + } + + for i, test := range tests { + fmt.Printf("Running RR client best only test #%d: %s\n", i+1, test.name) + for _, route := range test.routesAdd { + adjRIBOut.AddPath(route.Prefix(), route.Paths()[0]) + } + + for _, route := range test.routesRemove { + adjRIBOut.RemovePath(route.Prefix(), route.Paths()[0]) + } + + assert.Equal(t, test.expected, adjRIBOut.rt.Dump()) + + actualCount := adjRIBOut.RouteCount() + if test.expectedCount != actualCount { + t.Errorf("Expected route count %d differs from actual route count %d!\n", test.expectedCount, actualCount) + } + } +} + /* * Test for AddPath capabale peer / AdjRIBOut */ diff --git a/routingtable/neighbor.go b/routingtable/neighbor.go index bf6e97c6af46da590dd071e6e40b9978410f78ce..8b320636806792a2091cdc3cb4556c1b55a842bc 100644 --- a/routingtable/neighbor.go +++ b/routingtable/neighbor.go @@ -19,9 +19,15 @@ type Neighbor struct { // Local ASN of session LocalASN uint32 - // Peer is a route server client + // RouteServerClient incicates if the peer is a route server client RouteServerClient bool + // RouteReflectorClient indicates if the peer is a route reflector client + RouteReflectorClient bool + + // ClusterID is our route reflectors clusterID + ClusterID uint32 + // CapAddPathRX indicates if the peer supports receiving multiple BGP paths CapAddPathRX bool } diff --git a/routingtable/table.go b/routingtable/table.go index 16317e6fc50b7481c8e259be161b8a9bcba644a3..5fcb911c03f9706903c87ad77c80a010931b726b 100644 --- a/routingtable/table.go +++ b/routingtable/table.go @@ -71,13 +71,13 @@ func (rt *RoutingTable) RemovePath(pfx net.Prefix, p *route.Path) { rt.mu.Lock() defer rt.mu.Unlock() - if rt.removePath(pfx, p) { - atomic.AddInt64(&rt.routeCount, -1) - } + rt.removePath(pfx, p) } -func (rt *RoutingTable) removePath(pfx net.Prefix, p *route.Path) bool { - return rt.root.removePath(pfx, p) +func (rt *RoutingTable) removePath(pfx net.Prefix, p *route.Path) { + if rt.root.removePath(pfx, p) { + atomic.AddInt64(&rt.routeCount, -1) + } } func (rt *RoutingTable) removePaths(pfx net.Prefix, paths []*route.Path) {