diff --git a/BUILD.bazel b/BUILD.bazel
index ae2f1b1751ac95160a2e2097c520c3f99339211f..e246217503abd868585d80d2f8e1c6ee6cd3f679 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,4 +1,3 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 load("@bazel_gazelle//:def.bzl", "gazelle")
 
 # gazelle:prefix github.com/bio-routing/bio-rd
@@ -7,28 +6,3 @@ gazelle(
     external = "vendored",
     prefix = "github.com/bio-routing/bio-rd",
 )
-
-go_library(
-    name = "go_default_library",
-    srcs = [
-        "main.go",
-        "main_ipv4.go",
-    ],
-    importpath = "github.com/bio-routing/bio-rd",
-    visibility = ["//visibility:private"],
-    deps = [
-        "//config:go_default_library",
-        "//net:go_default_library",
-        "//protocols/bgp/server:go_default_library",
-        "//routingtable:go_default_library",
-        "//routingtable/filter:go_default_library",
-        "//routingtable/locRIB:go_default_library",
-        "//vendor/github.com/sirupsen/logrus:go_default_library",
-    ],
-)
-
-go_binary(
-    name = "bio-rd",
-    embed = [":go_default_library"],
-    visibility = ["//visibility:public"],
-)
diff --git a/examples/bgp/BUILD.bazel b/examples/bgp/BUILD.bazel
new file mode 100644
index 0000000000000000000000000000000000000000..d8bbb32432924040e60d2e9bdbab42bdf9e05ab1
--- /dev/null
+++ b/examples/bgp/BUILD.bazel
@@ -0,0 +1,26 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "main.go",
+        "main_ipv4.go",
+    ],
+    importpath = "github.com/bio-routing/bio-rd/examples/bgp",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//config:go_default_library",
+        "//net:go_default_library",
+        "//protocols/bgp/server:go_default_library",
+        "//routingtable:go_default_library",
+        "//routingtable/filter:go_default_library",
+        "//routingtable/locRIB:go_default_library",
+        "//vendor/github.com/sirupsen/logrus:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "bgp",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/main.go b/examples/bgp/main.go
similarity index 100%
rename from main.go
rename to examples/bgp/main.go
diff --git a/main_ipv4.go b/examples/bgp/main_ipv4.go
similarity index 100%
rename from main_ipv4.go
rename to examples/bgp/main_ipv4.go
diff --git a/main_ipv6.go b/examples/bgp/main_ipv6.go
similarity index 100%
rename from main_ipv6.go
rename to examples/bgp/main_ipv6.go
diff --git a/examples/bmp/BUILD.bazel b/examples/bmp/BUILD.bazel
new file mode 100644
index 0000000000000000000000000000000000000000..63c52adaba65907513eda97e260a79955d622d34
--- /dev/null
+++ b/examples/bmp/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["main_bmp.go"],
+    importpath = "github.com/bio-routing/bio-rd/examples/bmp",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//protocols/bmp/server:go_default_library",
+        "//routingtable/locRIB:go_default_library",
+        "//vendor/github.com/sirupsen/logrus:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "bmp",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/examples/bmp/main_bmp.go b/examples/bmp/main_bmp.go
new file mode 100644
index 0000000000000000000000000000000000000000..146fa4244f267d3fadfcf7fb1b5815524cfcc909
--- /dev/null
+++ b/examples/bmp/main_bmp.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+	"fmt"
+	"net"
+	"time"
+
+	"github.com/bio-routing/bio-rd/protocols/bmp/server"
+	"github.com/bio-routing/bio-rd/routingtable/locRIB"
+	"github.com/sirupsen/logrus"
+)
+
+func main() {
+	logrus.Printf("This is a BMP speaker\n")
+
+	rib := locRIB.New()
+	b := server.NewServer()
+	b.AddRouter(net.IP{127, 0, 0, 1}, 1234, rib, nil)
+
+	go func() {
+		for {
+			fmt.Printf("LocRIB count: %d\n", rib.Count())
+			time.Sleep(time.Second * 10)
+		}
+	}()
+
+	select {}
+}
diff --git a/fuzzing/packet/BUILD.bazel b/fuzzing/packet/BUILD.bazel
new file mode 100644
index 0000000000000000000000000000000000000000..86236b2a53413f8fbeb09531ade599310f84ff7d
--- /dev/null
+++ b/fuzzing/packet/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["gen_initial_corpus.go"],
+    importpath = "github.com/bio-routing/bio-rd/fuzzing/packet",
+    visibility = ["//visibility:private"],
+)
+
+go_binary(
+    name = "packet",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/protocols/bmp/packet/BUILD.bazel b/protocols/bmp/packet/BUILD.bazel
new file mode 100644
index 0000000000000000000000000000000000000000..599b586984491877b921ba92cb54d1367ea3e8c4
--- /dev/null
+++ b/protocols/bmp/packet/BUILD.bazel
@@ -0,0 +1,43 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "common_header.go",
+        "decode.go",
+        "information_tlv.go",
+        "initiation_message.go",
+        "peer_down.go",
+        "peer_up.go",
+        "per_peer_header.go",
+        "route_mirroring.go",
+        "route_monitoring.go",
+        "stats_report.go",
+        "termination_message.go",
+    ],
+    importpath = "github.com/bio-routing/bio-rd/protocols/bmp/packet",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//util/decoder:go_default_library",
+        "//vendor/github.com/taktv6/tflow2/convert:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = [
+        "common_header_test.go",
+        "decode_test.go",
+        "information_tlv_test.go",
+        "initiation_message_test.go",
+        "peer_down_test.go",
+        "peer_up_test.go",
+        "per_peer_header_test.go",
+        "route_mirroring_test.go",
+        "route_monitoring_test.go",
+        "stats_report_test.go",
+        "termination_message_test.go",
+    ],
+    embed = [":go_default_library"],
+    deps = ["//vendor/github.com/stretchr/testify/assert:go_default_library"],
+)
diff --git a/protocols/bmp/packet/common_header.go b/protocols/bmp/packet/common_header.go
new file mode 100644
index 0000000000000000000000000000000000000000..5b268355fb07c67ddb619fc5c1f04702bb338035
--- /dev/null
+++ b/protocols/bmp/packet/common_header.go
@@ -0,0 +1,43 @@
+package packet
+
+import (
+	"bytes"
+
+	"github.com/bio-routing/bio-rd/util/decoder"
+	"github.com/taktv6/tflow2/convert"
+)
+
+const (
+	// CommonHeaderLen is the length of a common header
+	CommonHeaderLen = 6
+)
+
+// CommonHeader represents a common header
+type CommonHeader struct {
+	Version   uint8
+	MsgLength uint32
+	MsgType   uint8
+}
+
+// Serialize serializes a common header
+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..d7989e28bb5955a294b195962d06c980cd0b645b
--- /dev/null
+++ b/protocols/bmp/packet/decode.go
@@ -0,0 +1,102 @@
+package packet
+
+import (
+	"bytes"
+	"fmt"
+)
+
+const (
+	// MinLen is the minimal length of a BMP message
+	MinLen = 6
+
+	RouteMonitoringType       = 0
+	StatisticsReportType      = 1
+	PeerDownNotificationType  = 2
+	PeerUpNotificationType    = 3
+	InitiationMessageType     = 4
+	TerminationMessageType    = 5
+	RouteMirroringMessageType = 6
+
+	BGPMessage     = 0
+	BGPInformation = 1
+
+	ErroredPDU  = 0
+	MessageLost = 1
+
+	// BMPVersion is the supported BMP version
+	BMPVersion = 3
+)
+
+// Msg is an interface that every BMP message must fulfill
+type Msg interface {
+	MsgType() uint8
+}
+
+// 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 != BMPVersion {
+		return nil, fmt.Errorf("Unsupported BMP version: %d", ch.Version)
+	}
+
+	switch ch.MsgType {
+	case RouteMonitoringType:
+		rm, err := decodeRouteMonitoringMsg(buf, ch)
+		if err != nil {
+			return nil, fmt.Errorf("Unable to decode route monitoring message: %v", err)
+		}
+
+		return rm, err
+	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 decode termination message: %v", err)
+		}
+
+		return tm, nil
+	case RouteMirroringMessageType:
+		rm, err := decodeRouteMirroringMsg(buf, ch)
+		if err != nil {
+			return nil, fmt.Errorf("Unable to decode route mirroring message: %v", err)
+		}
+
+		return rm, nil
+	default:
+		return nil, fmt.Errorf("Unexpected message type: %d", ch.MsgType)
+
+	}
+}
diff --git a/protocols/bmp/packet/decode_test.go b/protocols/bmp/packet/decode_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b3419725826e141b6225838f30d9e7d91296a2ba
--- /dev/null
+++ b/protocols/bmp/packet/decode_test.go
@@ -0,0 +1,453 @@
+package packet
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDecode(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []byte
+		wantFail bool
+		expected Msg
+	}{
+		{
+			name:     "incomplete common header",
+			input:    []byte{1, 2},
+			wantFail: true,
+		},
+		{
+			name:     "Invalid version",
+			input:    []byte{0, 0, 0, 0, 6, 5},
+			wantFail: true,
+		},
+		{
+			name: "Route monitoring ok",
+			input: []byte{
+				3, 0, 0, 0, 6 + 38 + 4, 0,
+
+				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,
+
+				1, 2, 3, 4,
+			},
+			wantFail: false,
+			expected: &RouteMonitoringMsg{
+				CommonHeader: &CommonHeader{
+					Version:   3,
+					MsgLength: 6 + 38 + 4,
+					MsgType:   0,
+				},
+				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,
+				},
+				BGPUpdate: []byte{1, 2, 3, 4},
+			},
+		},
+		{
+			name: "Route monitoring nok",
+			input: []byte{
+				3, 0, 0, 0, 6 + 38 + 4, 0,
+
+				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,
+
+				1, 2,
+			},
+			wantFail: true,
+		},
+		{
+			name: "Statistic report ok",
+			input: []byte{
+				3, 0, 0, 0, 6 + 9 + 38, 1,
+
+				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,
+
+				0, 0, 0, 1,
+				0, 1, 0, 1, 1,
+			},
+			wantFail: false,
+			expected: &StatsReport{
+				CommonHeader: &CommonHeader{
+					Version:   3,
+					MsgLength: 6 + 9 + 38,
+					MsgType:   1,
+				},
+				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: 1,
+				Stats: []*InformationTLV{
+					{
+						InformationType:   1,
+						InformationLength: 1,
+						Information:       []byte{1},
+					},
+				},
+			},
+		},
+		{
+			name: "Statistic report nok",
+			input: []byte{
+				3, 0, 0, 0, 6 + 9 + 38, 1,
+
+				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,
+			},
+			wantFail: true,
+		},
+		{
+			name: "peer down ok",
+			input: []byte{
+				3, 0, 0, 0, 6 + 9 + 38, 1,
+
+				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,
+
+				0, 0, 0, 1,
+				0, 1, 0, 1, 1,
+			},
+			wantFail: false,
+			expected: &StatsReport{
+				CommonHeader: &CommonHeader{
+					Version:   3,
+					MsgLength: 6 + 9 + 38,
+					MsgType:   1,
+				},
+				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: 1,
+				Stats: []*InformationTLV{
+					{
+						InformationType:   1,
+						InformationLength: 1,
+						Information:       []byte{1},
+					},
+				},
+			},
+		},
+		{
+			name: "peer down nok",
+			input: []byte{
+				3, 0, 0, 0, 6 + 9 + 38, 1,
+
+				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,
+
+				0, 0, 0, 1,
+				0, 1, 0, 1,
+			},
+			wantFail: true,
+		},
+		{
+			name: "peer up ok",
+			input: []byte{
+				3, 0, 0, 0, 54, 3,
+
+				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,
+
+				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
+			},
+			wantFail: false,
+			expected: &PeerUpNotification{
+				CommonHeader: &CommonHeader{
+					Version:   3,
+					MsgLength: 54,
+					MsgType:   3,
+				},
+				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,
+				},
+				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: "peer up nok",
+			input: []byte{
+				3, 0, 0, 0, 54, 3,
+
+				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,
+
+				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,
+			},
+			wantFail: true,
+		},
+		{
+			name: "initiation message ok",
+			input: []byte{
+				3, 0, 0, 0, 11, 4,
+
+				0, 1, 0, 1, 5,
+			},
+			wantFail: false,
+			expected: &InitiationMessage{
+				CommonHeader: &CommonHeader{
+					Version:   3,
+					MsgLength: 11,
+					MsgType:   4,
+				},
+				TLVs: []*InformationTLV{
+					{
+						InformationType:   1,
+						InformationLength: 1,
+						Information:       []byte{5},
+					},
+				},
+			},
+		},
+		{
+			name: "initiation message nok",
+			input: []byte{
+				3, 0, 0, 0,
+			},
+			wantFail: true,
+		},
+		{
+			name: "termination message ok",
+			input: []byte{
+				3, 0, 0, 0, 11, 5,
+
+				0, 1, 0, 1, 5,
+			},
+			wantFail: false,
+			expected: &TerminationMessage{
+				CommonHeader: &CommonHeader{
+					Version:   3,
+					MsgLength: 11,
+					MsgType:   5,
+				},
+				TLVs: []*InformationTLV{
+					{
+						InformationType:   1,
+						InformationLength: 1,
+						Information:       []byte{5},
+					},
+				},
+			},
+		},
+		{
+			name: "termination message nok",
+			input: []byte{
+				3, 0, 0, 0, 11, 5,
+
+				0, 1, 0,
+			},
+			wantFail: true,
+		},
+		{
+			name: "route mirror message ok",
+			input: []byte{
+				3, 0, 0, 0, 49, 6,
+
+				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,
+
+				0, 1, 0, 1, 5,
+			},
+			wantFail: false,
+			expected: &RouteMirroringMsg{
+				CommonHeader: &CommonHeader{
+					Version:   3,
+					MsgLength: 49,
+					MsgType:   6,
+				},
+				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,
+				},
+				TLVs: []*InformationTLV{
+					{
+						InformationType:   1,
+						InformationLength: 1,
+						Information:       []byte{5},
+					},
+				},
+			},
+		},
+		{
+			name: "route mirror message nok",
+			input: []byte{
+				3, 0, 0, 0, 49, 6,
+
+				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,
+			},
+			wantFail: true,
+		},
+		{
+			name: "invalid msg type",
+			input: []byte{
+				3, 0, 0, 0, 49, 7,
+			},
+			wantFail: true,
+		},
+	}
+
+	for _, test := range tests {
+		m, err := Decode(test.input)
+		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, m, "Test %q", test.name)
+	}
+}
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..f718a1b54f0aec63cee5d03311c292b089830986
--- /dev/null
+++ b/protocols/bmp/packet/information_tlv_test.go
@@ -0,0 +1,66 @@
+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 Value",
+			input: []byte{
+				0, 10, 0, 5,
+				1, 2, 3, 4,
+			},
+			wantFail: true,
+		},
+		{
+			name: "Incomplete Header",
+			input: []byte{
+				0, 10,
+			},
+			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..6fbc3f30c2c0f09e178b981964661990425ca597
--- /dev/null
+++ b/protocols/bmp/packet/initiation_message.go
@@ -0,0 +1,39 @@
+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
+}
+
+func decodeInitiationMessage(buf *bytes.Buffer, ch *CommonHeader) (Msg, error) {
+	im := &InitiationMessage{
+		CommonHeader: ch,
+		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
+	}
+
+	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..d5c3a129b7adc3a0ec46778d10f05da46b175ec9
--- /dev/null
+++ b/protocols/bmp/packet/initiation_message_test.go
@@ -0,0 +1,97 @@
+package packet
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestInitiationMessageMsgType(t *testing.T) {
+	pd := &InitiationMessage{
+		CommonHeader: &CommonHeader{
+			MsgType: 100,
+		},
+	}
+
+	if pd.MsgType() != 100 {
+		t.Errorf("Unexpected result")
+	}
+}
+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{
+				CommonHeader: &CommonHeader{
+					MsgLength: CommonHeaderLen + 17,
+				},
+				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..cdb72ffe33f72dc77e4c7373b6041a467bbbf836
--- /dev/null
+++ b/protocols/bmp/packet/peer_down.go
@@ -0,0 +1,64 @@
+package packet
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/bio-routing/bio-rd/util/decoder"
+)
+
+const (
+	reasonMin = 1
+	reasonMax = 3
+)
+
+// PeerDownNotification represents a peer down notification
+type PeerDownNotification struct {
+	CommonHeader  *CommonHeader
+	PerPeerHeader *PerPeerHeader
+	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{
+		CommonHeader: ch,
+	}
+
+	pph, err := decodePerPeerHeader(buf)
+	if err != nil {
+		return nil, fmt.Errorf("Unable to decode per peer header: %v", err)
+	}
+
+	p.PerPeerHeader = pph
+
+	fields := []interface{}{
+		&p.Reason,
+	}
+
+	err = decoder.Decode(buf, fields)
+	if err != nil {
+		return nil, err
+	}
+
+	if p.Reason < reasonMin || p.Reason > reasonMax {
+		return p, nil
+	}
+
+	p.Data = make([]byte, ch.MsgLength-PerPeerHeaderLen-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..b1df8b55e84d9ae6bf5854e4a2aea51357d73c3a
--- /dev/null
+++ b/protocols/bmp/packet/peer_down_test.go
@@ -0,0 +1,168 @@
+package packet
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPeerDownMsgType(t *testing.T) {
+	pd := &PeerDownNotification{
+		CommonHeader: &CommonHeader{
+			MsgType: 100,
+		},
+	}
+
+	if pd.MsgType() != 100 {
+		t.Errorf("Unexpected result")
+	}
+}
+
+func TestDecodePeerDownNotification(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []byte
+		ch       *CommonHeader
+		wantFail bool
+		expected *PeerDownNotification
+	}{
+		{
+			name: "Full",
+			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,
+
+				1,
+				1, 2, 3,
+			},
+			ch: &CommonHeader{
+				MsgLength: CommonHeaderLen + 4 + 38,
+			},
+			wantFail: false,
+			expected: &PeerDownNotification{
+				CommonHeader: &CommonHeader{
+					MsgLength: CommonHeaderLen + 4 + 38,
+				},
+				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,
+				},
+				Reason: 1,
+				Data: []byte{
+					1, 2, 3,
+				},
+			},
+		},
+		{
+			name: "Full no data",
+			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,
+				4,
+			},
+			ch: &CommonHeader{
+				MsgLength: CommonHeaderLen + PerPeerHeaderLen + 4,
+			},
+			wantFail: false,
+			expected: &PeerDownNotification{
+				CommonHeader: &CommonHeader{
+					MsgLength: CommonHeaderLen + PerPeerHeaderLen + 4,
+				},
+				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,
+				},
+				Reason: 4,
+				Data:   nil,
+			},
+		},
+		{
+			name: "Incomplete per peer header",
+			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,
+			},
+			ch: &CommonHeader{
+				MsgLength: CommonHeaderLen + 5,
+			},
+			wantFail: true,
+		},
+		{
+			name: "Incomplete data",
+			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,
+
+				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..58adc4cc5e6cd294c88758333f6aa26c214f46b0
--- /dev/null
+++ b/protocols/bmp/packet/peer_up.go
@@ -0,0 +1,109 @@
+package packet
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/bio-routing/bio-rd/util/decoder"
+)
+
+const (
+	// OpenMsgMinLen is the minimal length of a BGP open message
+	OpenMsgMinLen = 10
+)
+
+// PeerUpNotification represents a peer up notification
+type PeerUpNotification struct {
+	CommonHeader    *CommonHeader
+	PerPeerHeader   *PerPeerHeader
+	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{
+		CommonHeader: ch,
+	}
+
+	pph, err := decodePerPeerHeader(buf)
+	if err != nil {
+		return nil, fmt.Errorf("Unable to decode per peer header: %v", err)
+	}
+
+	p.PerPeerHeader = pph
+
+	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,
+	}
+
+	// This can not fail as p.Information has exactly the size of what is left in buf
+	decoder.Decode(buf, fields)
+
+	return p, nil
+}
+
+func getOpenMsg(buf *bytes.Buffer) ([]byte, error) {
+	msg := make([]byte, OpenMsgMinLen)
+
+	fields := []interface{}{
+		&msg,
+	}
+	err := decoder.Decode(buf, fields)
+	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])
+	fields = []interface{}{
+		&optParams,
+	}
+
+	err = decoder.Decode(buf, fields)
+	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..d29ec7550cc757ee8e3332d1de69509e590948bd
--- /dev/null
+++ b/protocols/bmp/packet/peer_up_test.go
@@ -0,0 +1,308 @@
+package packet
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPeerUpMsgType(t *testing.T) {
+	pd := &PeerUpNotification{
+		CommonHeader: &CommonHeader{
+			MsgType: 100,
+		},
+	}
+
+	if pd.MsgType() != 100 {
+		t.Errorf("Unexpected result")
+	}
+}
+func TestDecodePeerUp(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []byte
+		ch       *CommonHeader
+		wantFail bool
+		expected *PeerUpNotification
+	}{
+		{
+			name: "Full",
+			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,
+
+				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{
+				CommonHeader: &CommonHeader{
+					MsgLength: 47,
+				},
+				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,
+				},
+				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: "Full #2",
+			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,
+
+				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
+			},
+			ch: &CommonHeader{
+				MsgLength: 44,
+			},
+			wantFail: false,
+			expected: &PeerUpNotification{
+				CommonHeader: &CommonHeader{
+					MsgLength: 44,
+				},
+				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,
+				},
+				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
+				},
+			},
+		},
+		{
+			name: "Incomplete #0",
+			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,
+			},
+			ch: &CommonHeader{
+				MsgLength: 47,
+			},
+			wantFail: true,
+		},
+		{
+			name: "Incomplete #1",
+			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,
+
+				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,
+				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,
+
+				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,
+		},
+		{
+			name: "Incomplete #3",
+			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,
+				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,
+		},
+		{
+			name: "Incomplete #4",
+			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
+				3, // Opt Parm Len
+			},
+			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..d03f8b43d5d52b6dd705329c0bb10f834d39a15d
--- /dev/null
+++ b/protocols/bmp/packet/per_peer_header.go
@@ -0,0 +1,59 @@
+package packet
+
+import (
+	"bytes"
+
+	"github.com/bio-routing/bio-rd/util/decoder"
+	"github.com/taktv6/tflow2/convert"
+)
+
+const (
+	// PerPeerHeaderLen is the length of a per peer header
+	PerPeerHeaderLen = 38
+)
+
+// 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..e3d013a0f1d86248996613525236b58c88ab09ca
--- /dev/null
+++ b/protocols/bmp/packet/route_mirroring.go
@@ -0,0 +1,46 @@
+package packet
+
+import (
+	"bytes"
+	"fmt"
+)
+
+// RouteMirroringMsg represents a route mirroring message
+type RouteMirroringMsg struct {
+	CommonHeader  *CommonHeader
+	PerPeerHeader *PerPeerHeader
+	TLVs          []*InformationTLV
+}
+
+// MsgType returns the type of this message
+func (rm *RouteMirroringMsg) MsgType() uint8 {
+	return rm.CommonHeader.MsgType
+}
+
+func decodeRouteMirroringMsg(buf *bytes.Buffer, ch *CommonHeader) (*RouteMirroringMsg, error) {
+	rm := &RouteMirroringMsg{
+		CommonHeader: ch,
+	}
+
+	pph, err := decodePerPeerHeader(buf)
+	if err != nil {
+		return nil, fmt.Errorf("Unable to decode per peer header: %v", err)
+	}
+
+	rm.PerPeerHeader = pph
+
+	toRead := buf.Len()
+	read := 0
+
+	for read < toRead {
+		tlv, err := decodeInformationTLV(buf)
+		if err != nil {
+			return nil, fmt.Errorf("Unable to decode TLV: %v", err)
+		}
+
+		rm.TLVs = append(rm.TLVs, tlv)
+		read += int(tlv.InformationLength) + MinInformationTLVLen
+	}
+
+	return rm, nil
+}
diff --git a/protocols/bmp/packet/route_mirroring_test.go b/protocols/bmp/packet/route_mirroring_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..c62799a37a2c1bfa53c01eafb90979aa896716be
--- /dev/null
+++ b/protocols/bmp/packet/route_mirroring_test.go
@@ -0,0 +1,131 @@
+package packet
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRouteMirrorType(t *testing.T) {
+	pd := &RouteMirroringMsg{
+		CommonHeader: &CommonHeader{
+			MsgType: 100,
+		},
+	}
+
+	if pd.MsgType() != 100 {
+		t.Errorf("Unexpected result")
+	}
+}
+func TestDecodeRouteMirroringMsg(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []byte
+		ch       *CommonHeader
+		wantFail bool
+		expected *RouteMirroringMsg
+	}{
+		{
+			name: "Full",
+			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,
+
+				0, 1, 0, 2, 100, 200,
+				0, 1, 0, 2, 100, 200,
+			},
+			ch: &CommonHeader{
+				MsgLength: CommonHeaderLen + PerPeerHeaderLen + 12,
+			},
+			wantFail: false,
+			expected: &RouteMirroringMsg{
+				CommonHeader: &CommonHeader{
+					MsgLength: CommonHeaderLen + PerPeerHeaderLen + 12,
+				},
+				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,
+				},
+				TLVs: []*InformationTLV{
+					{
+						InformationType:   1,
+						InformationLength: 2,
+						Information:       []byte{100, 200},
+					},
+					{
+						InformationType:   1,
+						InformationLength: 2,
+						Information:       []byte{100, 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,
+			},
+			ch: &CommonHeader{
+				MsgLength: CommonHeaderLen + PerPeerHeaderLen + 12,
+			},
+			wantFail: true,
+		},
+		{
+			name: "Incomplete TLV",
+			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,
+
+				0, 1, 0, 2, 100, 200,
+				0, 1, 0, 2,
+			},
+			ch: &CommonHeader{
+				MsgLength: CommonHeaderLen + PerPeerHeaderLen + 12,
+			},
+			wantFail: true,
+		},
+	}
+
+	for _, test := range tests {
+		buf := bytes.NewBuffer(test.input)
+		r, err := decodeRouteMirroringMsg(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, r, "Test %q", test.name)
+	}
+}
diff --git a/protocols/bmp/packet/route_monitoring.go b/protocols/bmp/packet/route_monitoring.go
new file mode 100644
index 0000000000000000000000000000000000000000..d76aa453fd9ecb5869f4f6e1436db4db83029eee
--- /dev/null
+++ b/protocols/bmp/packet/route_monitoring.go
@@ -0,0 +1,46 @@
+package packet
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/bio-routing/bio-rd/util/decoder"
+)
+
+// RouteMonitoringMsg represents a Route Monitoring Message
+type RouteMonitoringMsg struct {
+	CommonHeader  *CommonHeader
+	PerPeerHeader *PerPeerHeader
+	BGPUpdate     []byte
+}
+
+// MsgType returns the type of this message
+func (rm *RouteMonitoringMsg) MsgType() uint8 {
+	return rm.CommonHeader.MsgType
+}
+
+func decodeRouteMonitoringMsg(buf *bytes.Buffer, ch *CommonHeader) (*RouteMonitoringMsg, error) {
+	rm := &RouteMonitoringMsg{
+		CommonHeader: ch,
+	}
+
+	pph, err := decodePerPeerHeader(buf)
+	if err != nil {
+		return nil, fmt.Errorf("Unable to decode per peer header: %v", err)
+	}
+
+	rm.PerPeerHeader = pph
+
+	rm.BGPUpdate = make([]byte, ch.MsgLength-CommonHeaderLen-PerPeerHeaderLen)
+
+	fields := []interface{}{
+		&rm.BGPUpdate,
+	}
+
+	err = decoder.Decode(buf, fields)
+	if err != nil {
+		return nil, err
+	}
+
+	return rm, nil
+}
diff --git a/protocols/bmp/packet/route_monitoring_test.go b/protocols/bmp/packet/route_monitoring_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..d28169346d6312f757fd317b879c55e20dd43847
--- /dev/null
+++ b/protocols/bmp/packet/route_monitoring_test.go
@@ -0,0 +1,100 @@
+package packet
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRouteMonitoringMsgType(t *testing.T) {
+	pd := &RouteMonitoringMsg{
+		CommonHeader: &CommonHeader{
+			MsgType: 100,
+		},
+	}
+
+	if pd.MsgType() != 100 {
+		t.Errorf("Unexpected result")
+	}
+}
+func TestDecodeRouteMonitoringMsg(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    []byte
+		ch       *CommonHeader
+		wantFail bool
+		expected *RouteMonitoringMsg
+	}{
+		{
+			name: "Full",
+			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,
+
+				100, 110, 120,
+			},
+			ch: &CommonHeader{
+				MsgLength: CommonHeaderLen + PerPeerHeaderLen + 3,
+			},
+			wantFail: false,
+			expected: &RouteMonitoringMsg{
+				CommonHeader: &CommonHeader{
+					MsgLength: CommonHeaderLen + PerPeerHeaderLen + 3,
+				},
+				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,
+				},
+				BGPUpdate: []byte{
+					100, 110, 120,
+				},
+			},
+		},
+		{
+			name: "Incomplete per peer header",
+			input: []byte{
+				1,
+				2,
+				0, 0, 0, 3,
+				1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+			},
+			ch: &CommonHeader{
+				MsgLength: CommonHeaderLen + PerPeerHeaderLen + 3,
+			},
+			wantFail: true,
+		},
+	}
+
+	for _, test := range tests {
+		buf := bytes.NewBuffer(test.input)
+		r, err := decodeRouteMonitoringMsg(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, r, "Test %q", test.name)
+	}
+}
diff --git a/protocols/bmp/packet/stats_report.go b/protocols/bmp/packet/stats_report.go
new file mode 100644
index 0000000000000000000000000000000000000000..3fa53995a48e6217a59a5a7ea7369dc71fc084a8
--- /dev/null
+++ b/protocols/bmp/packet/stats_report.go
@@ -0,0 +1,55 @@
+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
+}
+
+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..2fc47cbb4a1b69c55834af7db122215dfdf32a2b
--- /dev/null
+++ b/protocols/bmp/packet/stats_report_test.go
@@ -0,0 +1,150 @@
+package packet
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestStatsReportMsgType(t *testing.T) {
+	pd := &StatsReport{
+		CommonHeader: &CommonHeader{
+			MsgType: 100,
+		},
+	}
+
+	if pd.MsgType() != 100 {
+		t.Errorf("Unexpected result")
+	}
+}
+
+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..8021fbebacfce9b8f642c2493fedd39acc9a45f6
--- /dev/null
+++ b/protocols/bmp/packet/termination_message.go
@@ -0,0 +1,39 @@
+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{
+		CommonHeader: ch,
+		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..9f33bbfe0a07ef2de80ed2ce850c3742ed613916
--- /dev/null
+++ b/protocols/bmp/packet/termination_message_test.go
@@ -0,0 +1,97 @@
+package packet
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTerminationMessageType(t *testing.T) {
+	pd := &TerminationMessage{
+		CommonHeader: &CommonHeader{
+			MsgType: 100,
+		},
+	}
+
+	if pd.MsgType() != 100 {
+		t.Errorf("Unexpected result")
+	}
+}
+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{
+				CommonHeader: &CommonHeader{
+					MsgLength: CommonHeaderLen + 17,
+				},
+				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/BUILD.bazel b/protocols/bmp/server/BUILD.bazel
new file mode 100644
index 0000000000000000000000000000000000000000..922bed33e9464e8029fcb68dbbbf4bb4389827b1
--- /dev/null
+++ b/protocols/bmp/server/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "router.go",
+        "server.go",
+    ],
+    importpath = "github.com/bio-routing/bio-rd/protocols/bmp/server",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//protocols/bgp/packet:go_default_library",
+        "//protocols/bmp/packet:go_default_library",
+        "//routingtable/locRIB:go_default_library",
+        "//vendor/github.com/sirupsen/logrus:go_default_library",
+        "//vendor/github.com/taktv6/tflow2/convert:go_default_library",
+    ],
+)
diff --git a/protocols/bmp/server/router.go b/protocols/bmp/server/router.go
new file mode 100644
index 0000000000000000000000000000000000000000..3e54e25c6c750933a07e2a1899c4e57e2405104b
--- /dev/null
+++ b/protocols/bmp/server/router.go
@@ -0,0 +1,43 @@
+package server
+
+import (
+	"fmt"
+	"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
+	reconnectTimeMin int
+	reconnectTimeMax int
+	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
+		}
+
+		// TODO: Finish implementation
+		fmt.Printf("%v\n", bmpMsg)
+	}
+
+}
diff --git a/protocols/bmp/server/server.go b/protocols/bmp/server/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..2415929aaa1c7f92e6ba7e7d2683b1d8d496764a
--- /dev/null
+++ b/protocols/bmp/server/server.go
@@ -0,0 +1,90 @@
+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"
+	"github.com/taktv6/tflow2/convert"
+)
+
+const (
+	defaultBufferLen = 4096
+)
+
+type BMPServer struct {
+	routers       map[string]*router
+	routersMu     sync.RWMutex
+	reconnectTime uint
+}
+
+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,
+		reconnectTimeMin: 30,  // Suggested by RFC 7854
+		reconnectTimeMax: 720, // Suggested by RFC 7854
+		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 = r.reconnectTimeMin
+				} else if r.reconnectTime < r.reconnectTimeMax {
+					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 := convert.Uint32b(buffer[1:3])
+	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/BUILD.bazel b/util/decoder/BUILD.bazel
new file mode 100644
index 0000000000000000000000000000000000000000..42deb7927c538a3d38e1d2f16740a106be5193b1
--- /dev/null
+++ b/util/decoder/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["decoder.go"],
+    importpath = "github.com/bio-routing/bio-rd/util/decoder",
+    visibility = ["//visibility:public"],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["decoder_test.go"],
+    embed = [":go_default_library"],
+    deps = ["//vendor/github.com/stretchr/testify/assert:go_default_library"],
+)
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)
+}