diff --git a/main.go b/main_bmp.go similarity index 51% rename from main.go rename to main_bmp.go index 11c2a6895ade4237dbf48249437c2d202bb8aa58..146fa4244f267d3fadfcf7fb1b5815524cfcc909 100644 --- a/main.go +++ b/main_bmp.go @@ -2,27 +2,20 @@ package main import ( "fmt" + "net" "time" - "github.com/sirupsen/logrus" - - "github.com/bio-routing/bio-rd/protocols/bgp/server" + "github.com/bio-routing/bio-rd/protocols/bmp/server" "github.com/bio-routing/bio-rd/routingtable/locRIB" - - bnet "github.com/bio-routing/bio-rd/net" + "github.com/sirupsen/logrus" ) -func strAddr(s string) uint32 { - ret, _ := bnet.StrToAddr(s) - return ret -} - func main() { - logrus.Printf("This is a BGP speaker\n") + logrus.Printf("This is a BMP speaker\n") rib := locRIB.New() - b := server.NewBgpServer() - startServer(b, rib) + b := server.NewServer() + b.AddRouter(net.IP{127, 0, 0, 1}, 1234, rib, nil) go func() { for { diff --git a/protocols/bmp/packet/common_header.go b/protocols/bmp/packet/common_header.go new file mode 100644 index 0000000000000000000000000000000000000000..17d9a95e8da948f6b9cb90785c09a5ec0866ab86 --- /dev/null +++ b/protocols/bmp/packet/common_header.go @@ -0,0 +1,40 @@ +package packet + +import ( + "bytes" + + "github.com/bio-routing/bio-rd/util/decoder" + "github.com/bio-routing/tflow2/convert" +) + +const ( + CommonHeaderLen = 6 +) + +type CommonHeader struct { + Version uint8 + MsgLength uint32 + MsgType uint8 +} + +func (c *CommonHeader) Serialize(buf *bytes.Buffer) { + buf.WriteByte(c.Version) + buf.Write(convert.Uint32Byte(c.MsgLength)) + buf.WriteByte(c.MsgType) +} + +func decodeCommonHeader(buf *bytes.Buffer) (*CommonHeader, error) { + ch := &CommonHeader{} + fields := []interface{}{ + &ch.Version, + &ch.MsgLength, + &ch.MsgType, + } + + err := decoder.Decode(buf, fields) + if err != nil { + return ch, err + } + + return ch, nil +} diff --git a/protocols/bmp/packet/common_header_test.go b/protocols/bmp/packet/common_header_test.go new file mode 100644 index 0000000000000000000000000000000000000000..aec5942ed2f690641202a4e57fbf66da85680ec6 --- /dev/null +++ b/protocols/bmp/packet/common_header_test.go @@ -0,0 +1,81 @@ +package packet + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommonHeaderSerialize(t *testing.T) { + tests := []struct { + name string + input *CommonHeader + expected []byte + }{ + { + name: "Test #1", + input: &CommonHeader{ + Version: 3, + MsgLength: 100, + MsgType: 10, + }, + expected: []byte{3, 0, 0, 0, 100, 10}, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(nil) + test.input.Serialize(buf) + assert.Equalf(t, test.expected, buf.Bytes(), "Test %q", test.name) + } +} + +func TestDecodeCommonHeader(t *testing.T) { + tests := []struct { + name string + input []byte + wantFail bool + expected *CommonHeader + }{ + { + name: "Full packet", + input: []byte{ + 3, 0, 0, 0, 100, 10, + }, + wantFail: false, + expected: &CommonHeader{ + Version: 3, + MsgLength: 100, + MsgType: 10, + }, + }, + { + name: "Incomplete", + input: []byte{ + 3, 0, 0, 0, 100, + }, + wantFail: true, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(test.input) + ch, err := decodeCommonHeader(buf) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + continue + } + + assert.Equalf(t, test.expected, ch, "Test %q", test.name) + } +} diff --git a/protocols/bmp/packet/decode.go b/protocols/bmp/packet/decode.go new file mode 100644 index 0000000000000000000000000000000000000000..522e57a9ebcd814d32c188c935ee5a2122033434 --- /dev/null +++ b/protocols/bmp/packet/decode.go @@ -0,0 +1,89 @@ +package packet + +import ( + "bytes" + "fmt" +) + +type Msg interface { + MsgType() uint8 +} + +const ( + MinLen = 6 + + RouteMonitoringType = 0 + StatisticsReportType = 1 + PeerDownNotificationType = 2 + PeerUpNotificationType = 3 + InitiationMessageType = 4 + TerminationMessageType = 5 + RouteMirroringMessageType = 6 + + BGPMessage = 0 + BGPInformation = 1 + + ErroredPDU = 0 + MessageLost = 1 +) + +// Decode decodes a BMP message +func Decode(msg []byte) (Msg, error) { + buf := bytes.NewBuffer(msg) + + ch, err := decodeCommonHeader(buf) + if err != nil { + return nil, fmt.Errorf("Unable to decode common header: %v", err) + } + + if ch.Version != 3 { + return nil, fmt.Errorf("Unsupported BMP version: %d", ch.Version) + } + + switch ch.MsgType { + case RouteMonitoringType: + + case StatisticsReportType: + sr, err := decodeStatsReport(buf, ch) + if err != nil { + return nil, fmt.Errorf("Unable to decode stats report: %v", err) + } + + return sr, nil + case PeerDownNotificationType: + pd, err := decodePeerUpNotification(buf, ch) + if err != nil { + return nil, fmt.Errorf("Unable to decode peer down notification: %v", err) + } + + return pd, nil + case PeerUpNotificationType: + pu, err := decodePeerUpNotification(buf, ch) + if err != nil { + return nil, fmt.Errorf("Unable to decode peer up notification: %v", err) + } + + return pu, nil + case InitiationMessageType: + im, err := decodeInitiationMessage(buf, ch) + if err != nil { + return nil, fmt.Errorf("Unable to decode initiation message: %v", err) + } + + return im, nil + case TerminationMessageType: + tm, err := decodeTerminationMessage(buf, ch) + if err != nil { + return nil, fmt.Errorf("Unable to decide termination message: %v", err) + } + + return tm, nil + case RouteMirroringMessageType: + + default: + return nil, fmt.Errorf("Unexpected message type: %d", ch.MsgType) + + } + + return nil, fmt.Errorf("Unexpected message type: %d", ch.MsgType) +} diff --git a/protocols/bmp/packet/information_tlv.go b/protocols/bmp/packet/information_tlv.go new file mode 100644 index 0000000000000000000000000000000000000000..f939729fa93387b00422385fd40fcc5d778f555f --- /dev/null +++ b/protocols/bmp/packet/information_tlv.go @@ -0,0 +1,44 @@ +package packet + +import ( + "bytes" + + "github.com/bio-routing/bio-rd/util/decoder" +) + +const ( + MinInformationTLVLen = 4 +) + +// InformationTLV represents an information TLV +type InformationTLV struct { + InformationType uint16 + InformationLength uint16 + Information []byte +} + +func decodeInformationTLV(buf *bytes.Buffer) (*InformationTLV, error) { + infoTLV := &InformationTLV{} + + fields := []interface{}{ + &infoTLV.InformationType, + &infoTLV.InformationLength, + } + + err := decoder.Decode(buf, fields) + if err != nil { + return infoTLV, err + } + + infoTLV.Information = make([]byte, infoTLV.InformationLength) + fields = []interface{}{ + &infoTLV.Information, + } + + err = decoder.Decode(buf, fields) + if err != nil { + return infoTLV, err + } + + return infoTLV, nil +} diff --git a/protocols/bmp/packet/information_tlv_test.go b/protocols/bmp/packet/information_tlv_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2e42f5b0bac69a22d5f1cfd29695718dbf143348 --- /dev/null +++ b/protocols/bmp/packet/information_tlv_test.go @@ -0,0 +1,59 @@ +package packet + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeInformationTLV(t *testing.T) { + tests := []struct { + name string + input []byte + wantFail bool + expected *InformationTLV + }{ + { + name: "Full", + input: []byte{ + 0, 10, 0, 5, + 1, 2, 3, 4, 5, + }, + wantFail: false, + expected: &InformationTLV{ + InformationType: 10, + InformationLength: 5, + Information: []byte{1, 2, 3, 4, 5}, + }, + }, + { + name: "Incomplete", + input: []byte{ + 0, 10, 0, 5, + 1, 2, 3, 4, + }, + wantFail: true, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(test.input) + infoTLV, err := decodeInformationTLV(buf) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + continue + } + + assert.Equalf(t, test.expected, infoTLV, "Test %q", test.name) + } +} diff --git a/protocols/bmp/packet/initiation_message.go b/protocols/bmp/packet/initiation_message.go new file mode 100644 index 0000000000000000000000000000000000000000..6a3c02f6276f2bd610cfba56b31148427db55d90 --- /dev/null +++ b/protocols/bmp/packet/initiation_message.go @@ -0,0 +1,44 @@ +package packet + +import ( + "bytes" + "fmt" +) + +// InitiationMessage represents an initiation message +type InitiationMessage struct { + CommonHeader *CommonHeader + TLVs []*InformationTLV +} + +// MsgType returns the type of this message +func (im *InitiationMessage) MsgType() uint8 { + return im.CommonHeader.MsgType +} + +// SetCommonHeader sets the common header +func (im *InitiationMessage) SetCommonHeader(ch *CommonHeader) { + im.CommonHeader = ch +} + +func decodeInitiationMessage(buf *bytes.Buffer, ch *CommonHeader) (Msg, error) { + im := &InitiationMessage{ + TLVs: make([]*InformationTLV, 0, 2), + } + + read := uint32(0) + toRead := ch.MsgLength - CommonHeaderLen + + for read < toRead { + tlv, err := decodeInformationTLV(buf) + if err != nil { + return nil, fmt.Errorf("Unable to decode TLV: %v", err) + } + + im.TLVs = append(im.TLVs, tlv) + read += uint32(tlv.InformationLength) + MinInformationTLVLen + fmt.Printf("read: %d\n", read) + } + + return im, nil +} diff --git a/protocols/bmp/packet/initiation_message_test.go b/protocols/bmp/packet/initiation_message_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fdd327371f576a06ecc16aed06c254aeff96a163 --- /dev/null +++ b/protocols/bmp/packet/initiation_message_test.go @@ -0,0 +1,83 @@ +package packet + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeInitiationMessage(t *testing.T) { + tests := []struct { + name string + input []byte + ch *CommonHeader + wantFail bool + expected *InitiationMessage + }{ + { + name: "Full", + input: []byte{ + 0, 1, // sysDescr + 0, 4, // Length + 42, 42, 42, 42, // AAAA + 0, 2, //sysName + 0, 5, // Length + 43, 43, 43, 43, 43, // BBBBB + }, + ch: &CommonHeader{ + MsgLength: CommonHeaderLen + 17, + }, + wantFail: false, + expected: &InitiationMessage{ + TLVs: []*InformationTLV{ + { + InformationType: 1, + InformationLength: 4, + Information: []byte{42, 42, 42, 42}, + }, + { + InformationType: 2, + InformationLength: 5, + Information: []byte{43, 43, 43, 43, 43}, + }, + }, + }, + }, + { + name: "Incomplete", + input: []byte{ + 0, 1, // sysDescr + 0, 4, // Length + 42, 42, 42, 42, // AAAA + 0, 2, //sysName + 0, 5, // Length + 43, 43, 43, 43, // BBBB + }, + ch: &CommonHeader{ + MsgLength: CommonHeaderLen + 17, + }, + wantFail: true, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(test.input) + im, err := decodeInitiationMessage(buf, test.ch) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + continue + } + + assert.Equalf(t, test.expected, im, "Test %q", test.name) + } +} diff --git a/protocols/bmp/packet/peer_down.go b/protocols/bmp/packet/peer_down.go new file mode 100644 index 0000000000000000000000000000000000000000..347ffa2c51df7a371506183a30f3683df875795f --- /dev/null +++ b/protocols/bmp/packet/peer_down.go @@ -0,0 +1,48 @@ +package packet + +import ( + "bytes" + + "github.com/bio-routing/bio-rd/util/decoder" +) + +// PeerDownNotification represents a peer down notification +type PeerDownNotification struct { + CommonHeader *CommonHeader + Reason uint8 + Data []byte +} + +// MsgType returns the type of this message +func (p *PeerDownNotification) MsgType() uint8 { + return p.CommonHeader.MsgType +} + +func decodePeerDownNotification(buf *bytes.Buffer, ch *CommonHeader) (*PeerDownNotification, error) { + p := &PeerDownNotification{} + + fields := []interface{}{ + &p.Reason, + } + + err := decoder.Decode(buf, fields) + if err != nil { + return nil, err + } + + if p.Reason < 1 || p.Reason > 3 { + return p, nil + } + + p.Data = make([]byte, ch.MsgLength-CommonHeaderLen-1) + fields = []interface{}{ + p.Data, + } + + err = decoder.Decode(buf, fields) + if err != nil { + return nil, err + } + + return p, nil +} diff --git a/protocols/bmp/packet/peer_down_test.go b/protocols/bmp/packet/peer_down_test.go new file mode 100644 index 0000000000000000000000000000000000000000..01d7dac8db6dd65dbebbbf68b90ff5baf65ed420 --- /dev/null +++ b/protocols/bmp/packet/peer_down_test.go @@ -0,0 +1,89 @@ +package packet + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodePeerDownNotification(t *testing.T) { + tests := []struct { + name string + input []byte + ch *CommonHeader + wantFail bool + expected *PeerDownNotification + }{ + { + name: "Full", + input: []byte{ + 1, + 1, 2, 3, + }, + ch: &CommonHeader{ + MsgLength: CommonHeaderLen + 4, + }, + wantFail: false, + expected: &PeerDownNotification{ + Reason: 1, + Data: []byte{ + 1, 2, 3, + }, + }, + }, + { + name: "Full no data", + input: []byte{ + 4, + }, + ch: &CommonHeader{ + MsgLength: CommonHeaderLen + 4, + }, + wantFail: false, + expected: &PeerDownNotification{ + Reason: 4, + Data: nil, + }, + }, + { + name: "Incomplete data", + input: []byte{ + 1, + 1, 2, 3, + }, + ch: &CommonHeader{ + MsgLength: CommonHeaderLen + 5, + }, + wantFail: true, + }, + { + name: "Incomplete", + input: []byte{}, + ch: &CommonHeader{ + MsgLength: CommonHeaderLen + 5, + }, + wantFail: true, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(test.input) + p, err := decodePeerDownNotification(buf, test.ch) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + continue + } + + assert.Equalf(t, test.expected, p, "Test %q", test.name) + } +} diff --git a/protocols/bmp/packet/peer_up.go b/protocols/bmp/packet/peer_up.go new file mode 100644 index 0000000000000000000000000000000000000000..b8873b81a7c465ec0202a1a12ab40a8ca1f1b9b7 --- /dev/null +++ b/protocols/bmp/packet/peer_up.go @@ -0,0 +1,95 @@ +package packet + +import ( + "bytes" + "fmt" + + "github.com/bio-routing/bio-rd/util/decoder" +) + +const ( + OpenMsgMinLen = 10 +) + +// PeerUpNotification represents a peer up notification +type PeerUpNotification struct { + CommonHeader *CommonHeader + LocalAddress [16]byte + LocalPort uint16 + RemotePort uint16 + SentOpenMsg []byte + ReceivedOpenMsg []byte + Information []byte +} + +// MsgType returns the type of this message +func (p *PeerUpNotification) MsgType() uint8 { + return p.CommonHeader.MsgType +} + +func decodePeerUpNotification(buf *bytes.Buffer, ch *CommonHeader) (*PeerUpNotification, error) { + p := &PeerUpNotification{} + + fields := []interface{}{ + &p.LocalAddress, + &p.LocalPort, + &p.RemotePort, + } + + err := decoder.Decode(buf, fields) + if err != nil { + return nil, err + } + + sentOpenMsg, err := getOpenMsg(buf) + if err != nil { + return nil, fmt.Errorf("Unable to get OPEN message: %v", err) + } + p.SentOpenMsg = sentOpenMsg + + recvOpenMsg, err := getOpenMsg(buf) + if err != nil { + return nil, fmt.Errorf("Unable to get OPEN message: %v", err) + } + p.ReceivedOpenMsg = recvOpenMsg + + if buf.Len() == 0 { + return p, nil + } + + p.Information = make([]byte, buf.Len()) + fields = []interface{}{ + &p.Information, + } + + err = decoder.Decode(buf, fields) + if err != nil { + return nil, err + } + + fmt.Printf("%v\n", p.Information) + + return p, nil +} + +func getOpenMsg(buf *bytes.Buffer) ([]byte, error) { + msg := make([]byte, OpenMsgMinLen) + + _, err := buf.Read(msg) + if err != nil { + return nil, fmt.Errorf("Unable to read: %v", err) + } + + if msg[OpenMsgMinLen-1] == 0 { + return msg, nil + } + + optParams := make([]byte, msg[OpenMsgMinLen-1]) + _, err = buf.Read(optParams) + if err != nil { + return nil, fmt.Errorf("Unable to read: %v", err) + } + + msg = append(msg, optParams...) + return msg, nil +} diff --git a/protocols/bmp/packet/peer_up_test.go b/protocols/bmp/packet/peer_up_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c3c2c35f317ab2a08ae01423db74c8b11dc56448 --- /dev/null +++ b/protocols/bmp/packet/peer_up_test.go @@ -0,0 +1,120 @@ +package packet + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodePeerUp(t *testing.T) { + tests := []struct { + name string + input []byte + ch *CommonHeader + wantFail bool + expected *PeerUpNotification + }{ + { + name: "Full", + input: []byte{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 100, + 0, 200, + + // OPEN Sent + 4, // Version + 1, 0, // ASN + 2, 0, // Hold Time + 100, 110, 120, 130, // BGP Identifier + 5, // Opt Parm Len + 1, 2, 3, 4, 5, + + // OPEN Recv + 4, // Version + 1, 0, // ASN + 2, 0, // Hold Time + 100, 110, 120, 130, // BGP Identifier + 0, // Opt Parm Len + + 120, 140, 160, // Information + }, + ch: &CommonHeader{ + MsgLength: 47, + }, + wantFail: false, + expected: &PeerUpNotification{ + LocalAddress: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + LocalPort: 100, + RemotePort: 200, + SentOpenMsg: []byte{ + 4, // Version + 1, 0, // ASN + 2, 0, // Hold Time + 100, 110, 120, 130, // BGP Identifier + 5, // Opt Parm Len + 1, 2, 3, 4, 5, + }, + ReceivedOpenMsg: []byte{ + // OPEN Recv + 4, // Version + 1, 0, // ASN + 2, 0, // Hold Time + 100, 110, 120, 130, // BGP Identifier + 0, // Opt Parm Len + }, + Information: []byte{ + 120, 140, 160, // Information + }, + }, + }, + { + name: "Incomplete #1", + input: []byte{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 100, + }, + ch: &CommonHeader{ + MsgLength: 47, + }, + wantFail: true, + }, + { + name: "Incomplete #2", + input: []byte{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 100, + 0, 200, + + // OPEN Sent + 4, // Version + 1, 0, // ASN + 2, 0, // Hold Time + }, + ch: &CommonHeader{ + MsgLength: 47, + }, + wantFail: true, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(test.input) + pu, err := decodePeerUpNotification(buf, test.ch) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + continue + } + + assert.Equalf(t, test.expected, pu, "Test %q", test.name) + } +} diff --git a/protocols/bmp/packet/per_peer_header.go b/protocols/bmp/packet/per_peer_header.go new file mode 100644 index 0000000000000000000000000000000000000000..87231c86920a6729ce72a5de9cbb6a4f4f12e122 --- /dev/null +++ b/protocols/bmp/packet/per_peer_header.go @@ -0,0 +1,54 @@ +package packet + +import ( + "bytes" + + "github.com/bio-routing/bio-rd/util/decoder" + "github.com/bio-routing/tflow2/convert" +) + +// PerPeerHeader represents a BMP per peer header +type PerPeerHeader struct { + PeerType uint8 + PeerFlags uint8 + PeerDistinguisher uint32 + PeerAddress [16]byte + PeerAS uint32 + PeerBGPID uint32 + Timestamp uint32 + TimestampMicroSeconds uint32 +} + +// Serialize serializes a per peer header +func (p *PerPeerHeader) Serialize(buf *bytes.Buffer) { + buf.WriteByte(p.PeerType) + buf.WriteByte(p.PeerFlags) + buf.Write(convert.Uint32Byte(p.PeerDistinguisher)) + buf.Write(p.PeerAddress[:]) + buf.Write(convert.Uint32Byte(p.PeerAS)) + buf.Write(convert.Uint32Byte(p.PeerBGPID)) + buf.Write(convert.Uint32Byte(p.Timestamp)) + buf.Write(convert.Uint32Byte(p.TimestampMicroSeconds)) +} + +func decodePerPeerHeader(buf *bytes.Buffer) (*PerPeerHeader, error) { + p := &PerPeerHeader{} + + fields := []interface{}{ + &p.PeerType, + &p.PeerFlags, + &p.PeerDistinguisher, + &p.PeerAddress, + &p.PeerAS, + &p.PeerBGPID, + &p.Timestamp, + &p.TimestampMicroSeconds, + } + + err := decoder.Decode(buf, fields) + if err != nil { + return p, err + } + + return p, nil +} diff --git a/protocols/bmp/packet/per_peer_header_test.go b/protocols/bmp/packet/per_peer_header_test.go new file mode 100644 index 0000000000000000000000000000000000000000..de503a84edea465106787c5846ce26244c42c84a --- /dev/null +++ b/protocols/bmp/packet/per_peer_header_test.go @@ -0,0 +1,116 @@ +package packet + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPerPeerHeaderSerialize(t *testing.T) { + tests := []struct { + name string + input *PerPeerHeader + expected []byte + }{ + { + name: "Test #1", + input: &PerPeerHeader{ + PeerType: 1, + PeerFlags: 2, + PeerDistinguisher: 3, + PeerAddress: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + PeerAS: 51324, + PeerBGPID: 123, + Timestamp: 100, + TimestampMicroSeconds: 200, + }, + expected: []byte{ + 1, + 2, + 0, 0, 0, 3, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 0, 200, 124, + 0, 0, 0, 123, + 0, 0, 0, 100, + 0, 0, 0, 200, + }, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(nil) + test.input.Serialize(buf) + res := buf.Bytes() + + assert.Equalf(t, test.expected, res, "Test %q", test.name) + } +} + +func TestDecodePerPeerHeader(t *testing.T) { + tests := []struct { + name string + input []byte + wantFail bool + expected *PerPeerHeader + }{ + { + name: "Full packet", + input: []byte{ + 1, + 2, + 0, 0, 0, 3, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 0, 200, 124, + 0, 0, 0, 123, + 0, 0, 0, 100, + 0, 0, 0, 200, + }, + wantFail: false, + expected: &PerPeerHeader{ + PeerType: 1, + PeerFlags: 2, + PeerDistinguisher: 3, + PeerAddress: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + PeerAS: 51324, + PeerBGPID: 123, + Timestamp: 100, + TimestampMicroSeconds: 200, + }, + }, + { + name: "Incomplete", + input: []byte{ + 1, + 2, + 0, 0, 0, 3, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 0, 200, 124, + 0, 0, 0, 123, + 0, 0, 0, 100, + 0, 0, 0, + }, + wantFail: true, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(test.input) + p, err := decodePerPeerHeader(buf) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + continue + } + + assert.Equalf(t, test.expected, p, "Test %q", test.name) + } +} diff --git a/protocols/bmp/packet/route_mirroring.go b/protocols/bmp/packet/route_mirroring.go new file mode 100644 index 0000000000000000000000000000000000000000..9c40969f7215a143587237b30d213bdd01db4a74 --- /dev/null +++ b/protocols/bmp/packet/route_mirroring.go @@ -0,0 +1 @@ +package packet diff --git a/protocols/bmp/packet/route_monitoring.go b/protocols/bmp/packet/route_monitoring.go new file mode 100644 index 0000000000000000000000000000000000000000..9c40969f7215a143587237b30d213bdd01db4a74 --- /dev/null +++ b/protocols/bmp/packet/route_monitoring.go @@ -0,0 +1 @@ +package packet diff --git a/protocols/bmp/packet/stats_report.go b/protocols/bmp/packet/stats_report.go new file mode 100644 index 0000000000000000000000000000000000000000..c2c4e26366016dd5a49045957fc9f8661916bc7c --- /dev/null +++ b/protocols/bmp/packet/stats_report.go @@ -0,0 +1,60 @@ +package packet + +import ( + "bytes" + "fmt" + + "github.com/bio-routing/bio-rd/util/decoder" +) + +// StatsReport represents a stats report message +type StatsReport struct { + CommonHeader *CommonHeader + PerPeerHeader *PerPeerHeader + StatsCount uint32 + Stats []*InformationTLV +} + +// MsgType returns the type of this message +func (s *StatsReport) MsgType() uint8 { + return s.CommonHeader.MsgType +} + +// SetCommonHeader sets the common header +func (s *StatsReport) SetCommonHeader(ch *CommonHeader) { + s.CommonHeader = ch +} + +func decodeStatsReport(buf *bytes.Buffer, ch *CommonHeader) (Msg, error) { + sr := &StatsReport{ + CommonHeader: ch, + } + + pph, err := decodePerPeerHeader(buf) + if err != nil { + return nil, fmt.Errorf("Unable to decode per peer header: %v", err) + } + + sr.PerPeerHeader = pph + + fields := []interface{}{ + &sr.StatsCount, + } + + err = decoder.Decode(buf, fields) + if err != nil { + return sr, err + } + + sr.Stats = make([]*InformationTLV, sr.StatsCount) + for i := uint32(0); i < sr.StatsCount; i++ { + infoTLV, err := decodeInformationTLV(buf) + if err != nil { + return sr, fmt.Errorf("Unable to decode information TLV: %v", err) + } + + sr.Stats[i] = infoTLV + } + + return sr, nil +} diff --git a/protocols/bmp/packet/stats_report_test.go b/protocols/bmp/packet/stats_report_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f3a6329dbba9e7d38e90b6a8f2e97147fdc39f91 --- /dev/null +++ b/protocols/bmp/packet/stats_report_test.go @@ -0,0 +1,138 @@ +package packet + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeStatsReport(t *testing.T) { + tests := []struct { + name string + input []byte + wantFail bool + expected *StatsReport + }{ + { + name: "Full", + input: []byte{ + // Per Peer Header + 1, + 2, + 0, 0, 0, 3, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 0, 200, 124, + 0, 0, 0, 123, + 0, 0, 0, 100, + 0, 0, 0, 200, + + // Stats Count + 0, 0, 0, 2, + + 0, 1, + 0, 4, + 0, 0, 0, 2, + + 0, 2, + 0, 4, + 0, 0, 0, 3, + }, + wantFail: false, + expected: &StatsReport{ + PerPeerHeader: &PerPeerHeader{ + PeerType: 1, + PeerFlags: 2, + PeerDistinguisher: 3, + PeerAddress: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + PeerAS: 51324, + PeerBGPID: 123, + Timestamp: 100, + TimestampMicroSeconds: 200, + }, + StatsCount: 2, + Stats: []*InformationTLV{ + { + InformationType: 1, + InformationLength: 4, + Information: []byte{0, 0, 0, 2}, + }, + { + InformationType: 2, + InformationLength: 4, + Information: []byte{0, 0, 0, 3}, + }, + }, + }, + }, + { + name: "Incomplete per peer header", + input: []byte{ + // Per Peer Header + 1, + 2, + 0, 0, 0, 3, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 0, 200, 124, + 0, 0, 0, 123, + }, + wantFail: true, + }, + { + name: "Incomplete stats count", + input: []byte{ + // Per Peer Header + 1, + 2, + 0, 0, 0, 3, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 0, 200, 124, + 0, 0, 0, 123, + 0, 0, 0, 100, + 0, 0, 0, 200, + }, + wantFail: true, + }, + { + name: "Incomplete TLV", + input: []byte{ + // Per Peer Header + 1, + 2, + 0, 0, 0, 3, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 0, 0, 200, 124, + 0, 0, 0, 123, + 0, 0, 0, 100, + 0, 0, 0, 200, + + // Stats Count + 0, 0, 0, 2, + + 0, 1, + 0, 4, + }, + wantFail: true, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(test.input) + sr, err := decodeStatsReport(buf, nil) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + continue + } + + assert.Equalf(t, test.expected, sr, "Test %q", test.name) + } +} diff --git a/protocols/bmp/packet/termination_message.go b/protocols/bmp/packet/termination_message.go new file mode 100644 index 0000000000000000000000000000000000000000..8bf69afe3315ce38b26a4512f0ffc851c8a24af6 --- /dev/null +++ b/protocols/bmp/packet/termination_message.go @@ -0,0 +1,38 @@ +package packet + +import ( + "bytes" + "fmt" +) + +// TerminationMessage represents a termination message +type TerminationMessage struct { + CommonHeader *CommonHeader + TLVs []*InformationTLV +} + +// MsgType returns the type of this message +func (t *TerminationMessage) MsgType() uint8 { + return t.CommonHeader.MsgType +} + +func decodeTerminationMessage(buf *bytes.Buffer, ch *CommonHeader) (*TerminationMessage, error) { + tm := &TerminationMessage{ + TLVs: make([]*InformationTLV, 0, 2), + } + + read := uint32(0) + toRead := ch.MsgLength - CommonHeaderLen + + for read < toRead { + tlv, err := decodeInformationTLV(buf) + if err != nil { + return nil, fmt.Errorf("Unable to decode TLV: %v", err) + } + + tm.TLVs = append(tm.TLVs, tlv) + read += uint32(tlv.InformationLength) + MinInformationTLVLen + } + + return tm, nil +} diff --git a/protocols/bmp/packet/termination_message_test.go b/protocols/bmp/packet/termination_message_test.go new file mode 100644 index 0000000000000000000000000000000000000000..052290bdf74fc8b268aa4f18f275b10bde93c05d --- /dev/null +++ b/protocols/bmp/packet/termination_message_test.go @@ -0,0 +1,83 @@ +package packet + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecodeTerminationMessage(t *testing.T) { + tests := []struct { + name string + input []byte + ch *CommonHeader + wantFail bool + expected *TerminationMessage + }{ + { + name: "Full", + input: []byte{ + 0, 1, // sysDescr + 0, 4, // Length + 42, 42, 42, 42, // AAAA + 0, 2, //sysName + 0, 5, // Length + 43, 43, 43, 43, 43, // BBBBB + }, + ch: &CommonHeader{ + MsgLength: CommonHeaderLen + 17, + }, + wantFail: false, + expected: &TerminationMessage{ + TLVs: []*InformationTLV{ + { + InformationType: 1, + InformationLength: 4, + Information: []byte{42, 42, 42, 42}, + }, + { + InformationType: 2, + InformationLength: 5, + Information: []byte{43, 43, 43, 43, 43}, + }, + }, + }, + }, + { + name: "Incomplete", + input: []byte{ + 0, 1, // sysDescr + 0, 4, // Length + 42, 42, 42, 42, // AAAA + 0, 2, //sysName + 0, 5, // Length + 43, 43, 43, 43, // BBBB + }, + ch: &CommonHeader{ + MsgLength: CommonHeaderLen + 17, + }, + wantFail: true, + }, + } + + for _, test := range tests { + buf := bytes.NewBuffer(test.input) + im, err := decodeTerminationMessage(buf, test.ch) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + continue + } + + assert.Equalf(t, test.expected, im, "Test %q", test.name) + } +} diff --git a/protocols/bmp/server/router.go b/protocols/bmp/server/router.go new file mode 100644 index 0000000000000000000000000000000000000000..b720750f00724ae9d6b1ee3a56c513c756f2f7af --- /dev/null +++ b/protocols/bmp/server/router.go @@ -0,0 +1,38 @@ +package server + +import ( + "net" + "time" + + "github.com/bio-routing/bio-rd/protocols/bmp/packet" + "github.com/bio-routing/bio-rd/routingtable/locRIB" + log "github.com/sirupsen/logrus" +) + +type router struct { + address net.IP + port uint16 + con net.Conn + reconnectTime int + reconnectTimer *time.Timer + rib4 *locRIB.LocRIB + rib6 *locRIB.LocRIB +} + +func (r *router) serve() { + for { + msg, err := recvMsg(r.con) + if err != nil { + log.Errorf("Unable to get message: %v", err) + return + } + + bmpMsg, err := packet.Decode(msg) + if err != nil { + log.Errorf("Unable to decode BMP message: %v", err) + return + } + + } + +} diff --git a/protocols/bmp/server/server.go b/protocols/bmp/server/server.go new file mode 100644 index 0000000000000000000000000000000000000000..7116d0cab5a747f544c6f5301d3e61a1debb2474 --- /dev/null +++ b/protocols/bmp/server/server.go @@ -0,0 +1,87 @@ +package server + +import ( + "fmt" + "io" + "net" + "sync" + "time" + + "github.com/bio-routing/bio-rd/protocols/bgp/packet" + "github.com/bio-routing/bio-rd/routingtable/locRIB" + log "github.com/sirupsen/logrus" +) + +const ( + defaultBufferLen = 4096 +) + +type BMPServer struct { + routers map[string]*router + routersMu sync.RWMutex +} + +func NewServer() *BMPServer { + return &BMPServer{ + routers: make(map[string]*router), + } +} + +func (b *BMPServer) AddRouter(addr net.IP, port uint16, rib4 *locRIB.LocRIB, rib6 *locRIB.LocRIB) { + r := &router{ + address: addr, + port: port, + reconnectTime: 0, + reconnectTimer: time.NewTimer(time.Duration(0)), + rib4: rib4, + rib6: rib6, + } + + b.routersMu.Lock() + b.routers[fmt.Sprintf("%s:%d", r.address.String(), r.port)] = r + b.routersMu.Unlock() + + go func(r *router) { + for { + <-r.reconnectTimer.C + c, err := net.Dial("tcp", fmt.Sprintf("%s:%d", r.address.String(), r.port)) + if err != nil { + log.Infof("Unable to connect to BMP router: %v", err) + if r.reconnectTime == 0 { + r.reconnectTime = 30 + } else if r.reconnectTime < 720 { + r.reconnectTime *= 2 + } + r.reconnectTimer = time.NewTimer(time.Second * time.Duration(r.reconnectTime)) + continue + } + + r.reconnectTime = 0 + r.con = c + r.serve() + } + }(r) +} + +func recvMsg(c net.Conn) (msg []byte, err error) { + buffer := make([]byte, defaultBufferLen) + _, err = io.ReadFull(c, buffer[0:packet.MinLen]) + if err != nil { + return nil, fmt.Errorf("Read failed: %v", err) + } + + l := int(buffer[1])*256*256*256 + int(buffer[2])*256*256 + int(buffer[3])*256 + int(buffer[4]) + if l > defaultBufferLen { + tmp := buffer + buffer = make([]byte, l) + copy(buffer, tmp) + } + + toRead := l + _, err = io.ReadFull(c, buffer[packet.MinLen:toRead]) + if err != nil { + return nil, fmt.Errorf("Read failed: %v", err) + } + + return buffer, nil +} diff --git a/util/decoder/decoder.go b/util/decoder/decoder.go new file mode 100644 index 0000000000000000000000000000000000000000..709e39352a32471d241112a0c205a8a70b4e34bb --- /dev/null +++ b/util/decoder/decoder.go @@ -0,0 +1,19 @@ +package decoder + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +// Decode decodes network packets +func Decode(buf *bytes.Buffer, fields []interface{}) error { + var err error + for _, field := range fields { + err = binary.Read(buf, binary.BigEndian, field) + if err != nil { + return fmt.Errorf("Unable to read from buffer: %v", err) + } + } + return nil +} diff --git a/util/decoder/decoder_test.go b/util/decoder/decoder_test.go new file mode 100644 index 0000000000000000000000000000000000000000..acb13e220f32ce4f7032b4a432eea66390cb8d82 --- /dev/null +++ b/util/decoder/decoder_test.go @@ -0,0 +1,41 @@ +package decoder + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDecode(t *testing.T) { + input := []byte{ + 3, 0, 0, 0, 100, 200, + } + + type testData struct { + a uint8 + b uint32 + c []byte + } + + s := testData{ + c: make([]byte, 1), + } + + fields := []interface{}{ + &s.a, + &s.b, + &s.c, + } + + buf := bytes.NewBuffer(input) + Decode(buf, fields) + + expected := testData{ + a: 3, + b: 100, + c: []byte{200}, + } + + assert.Equal(t, expected, s) +}