From f4c235a52adc2990282b55ac09eb500e6844b531 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Tue, 30 Jun 2026 12:38:22 -0700 Subject: [PATCH 01/10] Add EoR marker to elem struct --- lib/bgpstream_elem.h | 3 +++ lib/bgpstream_filter.c | 2 ++ lib/bgpstream_filter.h | 1 + lib/bgpstream_record.c | 5 +++++ 4 files changed, 11 insertions(+) diff --git a/lib/bgpstream_elem.h b/lib/bgpstream_elem.h index 9ffc65b1..961dc881 100644 --- a/lib/bgpstream_elem.h +++ b/lib/bgpstream_elem.h @@ -124,6 +124,9 @@ typedef enum { /** Peer state change */ BGPSTREAM_ELEM_TYPE_PEERSTATE = 4, + /** End-of-RIB marker (RFC 4724) */ + BGPSTREAM_ELEM_TYPE_END_OF_RIB = 5, + } bgpstream_elem_type_t; typedef struct struct_bgpstream_annotations_t { diff --git a/lib/bgpstream_filter.c b/lib/bgpstream_filter.c index 920b9d2d..aea6e87f 100644 --- a/lib/bgpstream_filter.c +++ b/lib/bgpstream_filter.c @@ -123,6 +123,8 @@ int bgpstream_filter_mgr_filter_add(bgpstream_filter_mgr_t *this, this->elemtype_mask |= (BGPSTREAM_FILTER_ELEM_TYPE_WITHDRAWAL); } else if (strcmp(filter_value, "peerstates") == 0) { this->elemtype_mask |= (BGPSTREAM_FILTER_ELEM_TYPE_PEERSTATE); + } else if (strcmp(filter_value, "endofrib") == 0) { + this->elemtype_mask |= (BGPSTREAM_FILTER_ELEM_TYPE_END_OF_RIB); } else { bgpstream_log(BGPSTREAM_LOG_ERR, "unknown element type '%s'", filter_value); diff --git a/lib/bgpstream_filter.h b/lib/bgpstream_filter.h index 4a3d005a..33a39646 100644 --- a/lib/bgpstream_filter.h +++ b/lib/bgpstream_filter.h @@ -41,6 +41,7 @@ #define BGPSTREAM_FILTER_ELEM_TYPE_ANNOUNCEMENT 0x2 #define BGPSTREAM_FILTER_ELEM_TYPE_WITHDRAWAL 0x4 #define BGPSTREAM_FILTER_ELEM_TYPE_PEERSTATE 0x8 +#define BGPSTREAM_FILTER_ELEM_TYPE_END_OF_RIB 0x10 /* hash table community filter: * community -> filter mask (asn only, value only, both) */ diff --git a/lib/bgpstream_record.c b/lib/bgpstream_record.c index 06915164..d6e1348d 100644 --- a/lib/bgpstream_record.c +++ b/lib/bgpstream_record.c @@ -155,6 +155,11 @@ static int elem_check_filters(bgpstream_record_t *record, !(filter_mgr->elemtype_mask & BGPSTREAM_FILTER_ELEM_TYPE_WITHDRAWAL)) { return 0; } + + if (elem->type == BGPSTREAM_ELEM_TYPE_END_OF_RIB && + !(filter_mgr->elemtype_mask & BGPSTREAM_FILTER_ELEM_TYPE_END_OF_RIB)) { + return 0; + } } /* Checking peer ASNs: if the filter is on and the peer asn is not in the From 760af92794f3408850e06e92bab12e3911dcc56f Mon Sep 17 00:00:00 2001 From: Alistair King Date: Tue, 30 Jun 2026 12:58:19 -0700 Subject: [PATCH 02/10] Add parsing for EoR elems --- lib/formats/bgpstream_parsebgp_common.c | 57 +++++++++++++++++++++++++ lib/formats/bgpstream_parsebgp_common.h | 3 ++ lib/formats/bs_format_rislive.c | 1 + 3 files changed, 61 insertions(+) diff --git a/lib/formats/bgpstream_parsebgp_common.c b/lib/formats/bgpstream_parsebgp_common.c index 9d279ece..f48da963 100644 --- a/lib/formats/bgpstream_parsebgp_common.c +++ b/lib/formats/bgpstream_parsebgp_common.c @@ -310,6 +310,48 @@ static int handle_prefix(bgpstream_elem_t *elem, } \ } while (0) +// Detect an RFC 4724 End-of-RIB marker and, if found, populate `elem` as an +// END_OF_RIB elem whose prefix encodes the AFI (0.0.0.0/0 for IPv4 unicast, +// ::/0 for IPv6 unicast). Returns 1 if an EoR was detected and `elem` was +// populated, 0 otherwise. The caller guarantees the UPDATE carries no +// withdrawn/announced NLRIs before calling this. +static int handle_end_of_rib(bgpstream_elem_t *elem, + parsebgp_bgp_update_t *update) +{ + // a 16-byte zeroed source so we can build the 0.0.0.0/0 or ::/0 prefix that + // encodes the EoR AFI + static const uint8_t zero_addr[16] = { 0 }; + + // IPv4 unicast EoR: a completely empty UPDATE (no NLRIs, no path attrs) + if (update->path_attrs.attrs_cnt == 0) { + elem->type = BGPSTREAM_ELEM_TYPE_END_OF_RIB; + bgpstream_ipv4_addr_init(&elem->prefix.address, zero_addr); + elem->prefix.mask_len = 0; + return 1; + } + + // IPv6 (or other AFI) unicast EoR: an MP_UNREACH_NLRI attribute carrying zero + // withdrawn NLRIs with the unicast SAFI + parsebgp_bgp_update_path_attr_t *mp_unreach_attr = + &update->path_attrs.attrs[PARSEBGP_BGP_PATH_ATTR_TYPE_MP_UNREACH_NLRI]; + if (mp_unreach_attr->type != PARSEBGP_BGP_PATH_ATTR_TYPE_MP_UNREACH_NLRI) { + return 0; + } + parsebgp_bgp_update_mp_unreach_t *mp_unreach = mp_unreach_attr->data.mp_unreach; + if (mp_unreach->withdrawn_nlris_cnt != 0 || + mp_unreach->safi != PARSEBGP_BGP_SAFI_UNICAST) { + return 0; + } + elem->type = BGPSTREAM_ELEM_TYPE_END_OF_RIB; + if (mp_unreach->afi == PARSEBGP_BGP_AFI_IPV6) { + bgpstream_ipv6_addr_init(&elem->prefix.address, zero_addr); + } else { + bgpstream_ipv4_addr_init(&elem->prefix.address, zero_addr); + } + elem->prefix.mask_len = 0; + return 1; +} + int bgpstream_parsebgp_process_update(bgpstream_parsebgp_upd_state_t *upd_state, bgpstream_elem_t *elem, parsebgp_bgp_msg_t *bgp) @@ -325,6 +367,11 @@ int bgpstream_parsebgp_process_update(bgpstream_parsebgp_upd_state_t *upd_state, return 0; } + // the message claims to be an UPDATE, but carries no UPDATE payload + if (update == NULL) { + return 0; + } + // how many native withdrawals will we process? upd_state->withdrawal_v4_cnt = update->withdrawn_nlris.prefixes_cnt; @@ -355,6 +402,16 @@ int bgpstream_parsebgp_process_update(bgpstream_parsebgp_upd_state_t *upd_state, // are we at end-of-elems? if (upd_state->withdrawal_v4_cnt == 0 && upd_state->withdrawal_v6_cnt == 0 && upd_state->announce_v4_cnt == 0 && upd_state->announce_v6_cnt == 0) { + + // Check if this is an End-of-RIB marker (RFC 4724). We only emit a single + // EoR elem per record, so guard with eor_done. + if (upd_state->eor_done == 0 && update != NULL) { + upd_state->eor_done = 1; + if (handle_end_of_rib(elem, update) != 0) { + return 1; + } + } + return 0; } diff --git a/lib/formats/bgpstream_parsebgp_common.h b/lib/formats/bgpstream_parsebgp_common.h index 5f4210e7..6516dc02 100644 --- a/lib/formats/bgpstream_parsebgp_common.h +++ b/lib/formats/bgpstream_parsebgp_common.h @@ -86,6 +86,9 @@ typedef struct bgpstream_parsebgp_upd_state { // has the BGP4MP state been prepared int ready; + // has the End-of-RIB check been performed + int eor_done; + // how many native (IPv4) withdrawals still to yield int withdrawal_v4_cnt; int withdrawal_v4_idx; diff --git a/lib/formats/bs_format_rislive.c b/lib/formats/bs_format_rislive.c index 53b7b267..bdfad1ee 100644 --- a/lib/formats/bs_format_rislive.c +++ b/lib/formats/bs_format_rislive.c @@ -682,6 +682,7 @@ void bs_format_rislive_clear_data(bgpstream_format_t *format, void *data) bgpstream_elem_clear(rd->elem); rd->end_of_elems = 0; rd->next_re = 0; + rd->msg_type = 0; bgpstream_parsebgp_upd_state_reset(&rd->upd_state); parsebgp_clear_msg(rd->msg); } From d6ec90a52e11b43da6b68f0c11e071628a827cc0 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Tue, 30 Jun 2026 13:08:28 -0700 Subject: [PATCH 03/10] Update ascii output formats --- lib/bgpstream_bgpdump.c | 10 ++++++++++ lib/bgpstream_elem.c | 25 +++++++++++++++++++++++++ tools/bgpreader.c | 6 ++++++ 3 files changed, 41 insertions(+) diff --git a/lib/bgpstream_bgpdump.c b/lib/bgpstream_bgpdump.c index 5101b564..0e3f9f85 100644 --- a/lib/bgpstream_bgpdump.c +++ b/lib/bgpstream_bgpdump.c @@ -67,6 +67,16 @@ char *bgpstream_record_elem_bgpdump_snprintf(char *buf, size_t len, ssize_t c = 0; /* < how many chars were written */ char *buf_p = buf; + /* The original bgpdump tool does not represent End-of-RIB markers in its + * output (an EoR is an empty UPDATE, which it silently drops). To stay + * faithful to that format we emit nothing for them. Callers should skip empty + * output. */ + if (elem->type == BGPSTREAM_ELEM_TYPE_END_OF_RIB) { + if (len > 0) + buf[0] = '\0'; + return buf; + } + /* Record type */ switch (elem->type) { case BGPSTREAM_ELEM_TYPE_RIB: diff --git a/lib/bgpstream_elem.c b/lib/bgpstream_elem.c index 9f28b671..923e5b40 100644 --- a/lib/bgpstream_elem.c +++ b/lib/bgpstream_elem.c @@ -129,6 +129,7 @@ int bgpstream_elem_type_snprintf(char *buf, size_t len, case BGPSTREAM_ELEM_TYPE_ANNOUNCEMENT: ch = 'A'; break; case BGPSTREAM_ELEM_TYPE_WITHDRAWAL: ch = 'W'; break; case BGPSTREAM_ELEM_TYPE_PEERSTATE: ch = 'S'; break; + case BGPSTREAM_ELEM_TYPE_END_OF_RIB: ch = 'E'; break; default: if (len > 0) buf[0] = '\0'; @@ -378,6 +379,30 @@ char *bgpstream_elem_custom_snprintf(char *buf, size_t len, /* END OF LINE */ break; + case BGPSTREAM_ELEM_TYPE_END_OF_RIB: + + /* PREFIX (AFI indicator) */ + if (bgpstream_pfx_snprintf(buf_p, B_REMAIN, &(elem->prefix)) == NULL) { + if (errno != ENOSPC) + bgpstream_log(BGPSTREAM_LOG_ERR, "Malformed prefix (E)"); + return NULL; + } + SEEK_STR_END; + ADD_PIPE; + /* NEXT HOP (empty) */ + ADD_PIPE; + /* AS PATH (empty) */ + ADD_PIPE; + /* ORIGIN AS (empty) */ + ADD_PIPE; + /* COMMUNITIES (empty) */ + ADD_PIPE; + /* OLD STATE (empty) */ + ADD_PIPE; + /* NEW STATE (empty) */ + /* END OF LINE */ + break; + default: bgpstream_log(BGPSTREAM_LOG_ERR, "Error during elem processing"); return NULL; diff --git a/tools/bgpreader.c b/tools/bgpreader.c index 189fe08e..a8d893cd 100644 --- a/tools/bgpreader.c +++ b/tools/bgpreader.c @@ -771,6 +771,12 @@ static int print_elem_bgpdump(bgpstream_record_t *record, return -1; } + /* Some elem types (e.g. End-of-RIB markers) have no representation in the + * bgpdump output format and produce an empty string; skip them. */ + if (buf[0] == '\0') { + return 0; + } + printf("%s\n", buf); return 0; } From 2c1e2527f16c5a1774151a3d9c697a77efdae843 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Tue, 30 Jun 2026 13:11:40 -0700 Subject: [PATCH 04/10] Update doc --- tools/bgpreader.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/bgpreader.c b/tools/bgpreader.c index a8d893cd..0b333b1c 100644 --- a/tools/bgpreader.c +++ b/tools/bgpreader.c @@ -66,7 +66,8 @@ "|||\n" \ "#\n" \ "# : R RIB, U Update\n" \ - "# : R RIB, A announcement, W withdrawal, S state message\n" \ + "# : R RIB, A announcement, W withdrawal, S state message,\n" \ + "# E end-of-RIB marker\n" \ "#\n" enum rpki_options { From fd129c6f8573746361c7c6ca3f24316cdb83e45d Mon Sep 17 00:00:00 2001 From: Alistair King Date: Tue, 30 Jun 2026 13:49:29 -0700 Subject: [PATCH 05/10] Fix bugs --- lib/bgpstream_elem.c | 9 +++++++++ lib/formats/bgpstream_parsebgp_common.c | 12 ++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/bgpstream_elem.c b/lib/bgpstream_elem.c index 923e5b40..3ab8785b 100644 --- a/lib/bgpstream_elem.c +++ b/lib/bgpstream_elem.c @@ -92,6 +92,15 @@ void bgpstream_elem_clear(bgpstream_elem_t *elem) { bgpstream_as_path_clear(elem->as_path); bgpstream_community_set_clear(elem->communities); + + /* reset the remaining type-dependent fields so that a re-used elem does not + * leak stale values (e.g. path attributes from a previous elem) */ + elem->nexthop.version = BGPSTREAM_ADDR_VERSION_UNKNOWN; + elem->has_origin = 0; + elem->has_med = 0; + elem->has_local_pref = 0; + elem->atomic_aggregate = 0; + elem->aggregator.has_aggregator = 0; } bgpstream_elem_t *bgpstream_elem_copy(bgpstream_elem_t *dst, diff --git a/lib/formats/bgpstream_parsebgp_common.c b/lib/formats/bgpstream_parsebgp_common.c index f48da963..487e1e5a 100644 --- a/lib/formats/bgpstream_parsebgp_common.c +++ b/lib/formats/bgpstream_parsebgp_common.c @@ -322,8 +322,15 @@ static int handle_end_of_rib(bgpstream_elem_t *elem, // encodes the EoR AFI static const uint8_t zero_addr[16] = { 0 }; - // IPv4 unicast EoR: a completely empty UPDATE (no NLRIs, no path attrs) - if (update->path_attrs.attrs_cnt == 0) { + // IPv4 unicast EoR: a completely empty UPDATE (no withdrawn or announced + // NLRIs, and no path attributes). Note that we must check the original NLRI + // counts (not the upd_state counters, which have been decremented to zero as + // elems were yielded) so that we don't mistake a withdrawal-only UPDATE for + // an EoR. + if (update->path_attrs.attrs_cnt == 0 && + update->withdrawn_nlris.prefixes_cnt == 0 && + update->announced_nlris.prefixes_cnt == 0) { + bgpstream_elem_clear(elem); elem->type = BGPSTREAM_ELEM_TYPE_END_OF_RIB; bgpstream_ipv4_addr_init(&elem->prefix.address, zero_addr); elem->prefix.mask_len = 0; @@ -342,6 +349,7 @@ static int handle_end_of_rib(bgpstream_elem_t *elem, mp_unreach->safi != PARSEBGP_BGP_SAFI_UNICAST) { return 0; } + bgpstream_elem_clear(elem); elem->type = BGPSTREAM_ELEM_TYPE_END_OF_RIB; if (mp_unreach->afi == PARSEBGP_BGP_AFI_IPV6) { bgpstream_ipv6_addr_init(&elem->prefix.address, zero_addr); From 70c37871fa9a2eb30a9e92e38420a5a030e1b613 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Tue, 30 Jun 2026 14:01:41 -0700 Subject: [PATCH 06/10] Fix another review comment --- lib/formats/bgpstream_parsebgp_common.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/formats/bgpstream_parsebgp_common.c b/lib/formats/bgpstream_parsebgp_common.c index 487e1e5a..707d9edd 100644 --- a/lib/formats/bgpstream_parsebgp_common.c +++ b/lib/formats/bgpstream_parsebgp_common.c @@ -337,8 +337,14 @@ static int handle_end_of_rib(bgpstream_elem_t *elem, return 1; } - // IPv6 (or other AFI) unicast EoR: an MP_UNREACH_NLRI attribute carrying zero - // withdrawn NLRIs with the unicast SAFI + // IPv4/IPv6 unicast EoR: the UPDATE carries exactly one path attribute, an + // empty MP_UNREACH_NLRI (zero withdrawn NLRIs) for the IPv4 or IPv6 unicast + // address family. Requiring MP_UNREACH to be the *only* attribute (and the + // AFI/SAFI to be one we recognise) avoids misclassifying malformed UPDATEs or + // non-IPv4/IPv6 families as an EoR. + if (update->path_attrs.attrs_cnt != 1) { + return 0; + } parsebgp_bgp_update_path_attr_t *mp_unreach_attr = &update->path_attrs.attrs[PARSEBGP_BGP_PATH_ATTR_TYPE_MP_UNREACH_NLRI]; if (mp_unreach_attr->type != PARSEBGP_BGP_PATH_ATTR_TYPE_MP_UNREACH_NLRI) { @@ -353,8 +359,11 @@ static int handle_end_of_rib(bgpstream_elem_t *elem, elem->type = BGPSTREAM_ELEM_TYPE_END_OF_RIB; if (mp_unreach->afi == PARSEBGP_BGP_AFI_IPV6) { bgpstream_ipv6_addr_init(&elem->prefix.address, zero_addr); - } else { + } else if (mp_unreach->afi == PARSEBGP_BGP_AFI_IPV4) { bgpstream_ipv4_addr_init(&elem->prefix.address, zero_addr); + } else { + // some other (unsupported) address family - not an EoR we represent + return 0; } elem->prefix.mask_len = 0; return 1; From d36de45d13216875515636ec47277bd5bccff3f7 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Tue, 30 Jun 2026 14:12:44 -0700 Subject: [PATCH 07/10] Add unit tests --- .gitignore | 1 + test/Makefile.am | 6 ++ test/bgpstream-test-eor.c | 158 ++++++++++++++++++++++++++++++++++++++ test/eor-stream.json | 8 ++ 4 files changed, 173 insertions(+) create mode 100644 test/bgpstream-test-eor.c create mode 100644 test/eor-stream.json diff --git a/.gitignore b/.gitignore index e9acf86c..818df744 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ tools/bgpreader test/bgpstream-test test/bgpstream-test-filters test/bgpstream-test-rislive +test/bgpstream-test-eor test/bgpstream-test-rpki test/bgpstream-test-utils-addr test/bgpstream-test-utils-pfx diff --git a/test/Makefile.am b/test/Makefile.am index 829ea961..182e4bb0 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -36,6 +36,7 @@ TESTS = \ bgpstream-test \ bgpstream-test-filters \ bgpstream-test-rislive \ + bgpstream-test-eor \ bgpstream-test-utils-addr \ bgpstream-test-utils-pfx \ bgpstream-test-utils-patricia \ @@ -46,6 +47,7 @@ check_PROGRAMS = \ bgpstream-test \ bgpstream-test-filters \ bgpstream-test-rislive \ + bgpstream-test-eor \ bgpstream-test-utils-addr \ bgpstream-test-utils-pfx \ bgpstream-test-utils-patricia \ @@ -56,6 +58,7 @@ check_PROGRAMS = \ EXTRA_DIST = sqlite_test.db \ csv_test.csv \ ris-live-stream.json \ + eor-stream.json \ routeviews.route-views.jinx.ribs.1427846400.bz2 \ routeviews.route-views.jinx.updates.1427846400.bz2 \ ris.rrc06.updates.1427846400.gz \ @@ -70,6 +73,9 @@ bgpstream_test_filters_LDADD = $(top_builddir)/lib/libbgpstream.la bgpstream_test_rislive_SOURCES = bgpstream-test-rislive.c bgpstream_test.h bgpstream_test_rislive_LDADD = $(top_builddir)/lib/libbgpstream.la +bgpstream_test_eor_SOURCES = bgpstream-test-eor.c bgpstream_test.h +bgpstream_test_eor_LDADD = $(top_builddir)/lib/libbgpstream.la + bgpstream_test_rpki_SOURCES = bgpstream-test-rpki.c bgpstream-test-rpki.h bgpstream_test.h bgpstream_test_rpki_LDADD = $(top_builddir)/lib/libbgpstream.la diff --git a/test/bgpstream-test-eor.c b/test/bgpstream-test-eor.c new file mode 100644 index 00000000..8bd6ea4f --- /dev/null +++ b/test/bgpstream-test-eor.c @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2014 The Regents of the University of California. + * + * This file is part of libbgpstream. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "bgpstream.h" +#include "bgpstream_test.h" +#include "utils.h" +#include +#include + +/* + * End-of-RIB detection test. + * + * The fixture eor-stream.json feeds a series of RIS Live UPDATE messages (as + * raw on-the-wire BGP bytes) through the full decode pipeline and checks that + * each is or is not classified as an RFC 4724 End-of-RIB marker. The raw bytes + * were hand-built per RFC 4271 (UPDATE) / RFC 4760 (MP_(UN)REACH_NLRI) / RFC + * 4724 (End-of-RIB) and then independently cross-checked with scapy's BGP + * dissector, which shares no code with libbgpstream / libparsebgp. + * + * The set deliberately mixes genuine EoR markers with non-EoR messages that + * sit close to the detection boundary (including the false-positive cases fixed + * during review), to guard against both missed and spurious EoR classification: + * + * 0. empty IPv4 UPDATE -> EoR (0.0.0.0/0) + * FF*16 0017 02 0000 0000 + * 1. empty MP_UNREACH, IPv6 unicast -> EoR (::/0) + * ... 800F 03 0002 01 + * 2. empty MP_UNREACH, IPv4 unicast -> EoR (0.0.0.0/0) + * ... 800F 03 0001 01 + * 3. native IPv4 withdrawal 10.0.0.0/8 -> WITHDRAWAL (NOT EoR) + * a withdrawal-only UPDATE must not be mistaken for an EoR + * 4. MP_UNREACH withdrawal 2001:db8::/32 -> WITHDRAWAL (NOT EoR) + * a non-empty MP_UNREACH must not be mistaken for an EoR + * 5. empty MP_UNREACH, IPv6 multicast (SAFI=2) -> no elems (NOT EoR) + * EoR is restricted to the unicast SAFI + * 6. empty MP_UNREACH unicast + extra ORIGIN attr -> no elems (NOT EoR) + * EoR requires MP_UNREACH to be the only path attribute + * 7. normal IPv4 announcement 192.0.2.0/24 -> ANNOUNCEMENT (NOT EoR) + * a routine announcement must not be mistaken for an EoR + */ + +#define MAX_ELEMS 2 + +typedef struct { + const char *desc; + int n_elems; + bgpstream_elem_type_t types[MAX_ELEMS]; + const char *prefixes[MAX_ELEMS]; +} expected_record_t; + +static const expected_record_t expected[] = { + { "empty IPv4 UPDATE -> EoR", 1, + { BGPSTREAM_ELEM_TYPE_END_OF_RIB }, { "0.0.0.0/0" } }, + { "MP_UNREACH IPv6 unicast empty -> EoR", 1, + { BGPSTREAM_ELEM_TYPE_END_OF_RIB }, { "::/0" } }, + { "MP_UNREACH IPv4 unicast empty -> EoR", 1, + { BGPSTREAM_ELEM_TYPE_END_OF_RIB }, { "0.0.0.0/0" } }, + { "native IPv4 withdrawal -> W (not EoR)", 1, + { BGPSTREAM_ELEM_TYPE_WITHDRAWAL }, { "10.0.0.0/8" } }, + { "MP_UNREACH IPv6 withdrawal -> W (not EoR)", 1, + { BGPSTREAM_ELEM_TYPE_WITHDRAWAL }, { "2001:db8::/32" } }, + { "MP_UNREACH non-unicast -> no elems (not EoR)", 0, { 0 }, { NULL } }, + { "MP_UNREACH + extra attr -> no elems (not EoR)", 0, { 0 }, { NULL } }, + { "IPv4 announcement -> A (not EoR)", 1, + { BGPSTREAM_ELEM_TYPE_ANNOUNCEMENT }, { "192.0.2.0/24" } }, +}; + +#define N_RECORDS (int)(sizeof(expected) / sizeof(expected[0])) + +static char buf[1024]; + +static int test_eor(void) +{ + int rrc, erc, rcount = 0; + bgpstream_t *bs = bgpstream_create(); + bgpstream_elem_t *elem; + bgpstream_record_t *rec = NULL; + bgpstream_data_interface_id_t di_id; + bgpstream_data_interface_option_t *option; + + di_id = bgpstream_get_data_interface_id_by_name(bs, "singlefile"); + bgpstream_set_data_interface(bs, di_id); + + option = bgpstream_get_data_interface_option_by_name(bs, di_id, "upd-type"); + CHECK("get upd-type option", option != NULL); + CHECK("set upd-type ris-live", + bgpstream_set_data_interface_option(bs, option, "ris-live") == 0); + + option = bgpstream_get_data_interface_option_by_name(bs, di_id, "upd-file"); + CHECK("get upd-file option", option != NULL); + CHECK("set upd-file", + bgpstream_set_data_interface_option(bs, option, "eor-stream.json") == 0); + + CHECK("stream start", bgpstream_start(bs) == 0); + + while ((rrc = bgpstream_get_next_record(bs, &rec)) > 0) { + if (rec->status != BGPSTREAM_RECORD_STATUS_VALID_RECORD) { + continue; + } + + CHECK("record index within expected range", rcount < N_RECORDS); + if (rcount >= N_RECORDS) { + break; + } + const expected_record_t *exp = &expected[rcount]; + + int e = 0; + while ((erc = bgpstream_record_get_next_elem(rec, &elem)) > 0) { + CHECK_MSG(exp->desc, "more elems than expected", e < exp->n_elems); + if (e >= exp->n_elems) { + e++; + continue; + } + CHECK_MSG(exp->desc, "elem type", elem->type == exp->types[e]); + CHECK(exp->desc, + bgpstream_pfx_snprintf(buf, sizeof(buf), &elem->prefix) != NULL); + CHECK_MSG(exp->desc, exp->prefixes[e], + strcmp(buf, exp->prefixes[e]) == 0); + e++; + } + CHECK_MSG(exp->desc, "elem count", e == exp->n_elems); + rcount++; + } + + CHECK("final return code", rrc == 0); + CHECK("read all records", rcount == N_RECORDS); + + bgpstream_destroy(bs); + return 0; +} + +int main(void) +{ + test_eor(); + ENDTEST; + return 0; +} diff --git a/test/eor-stream.json b/test/eor-stream.json new file mode 100644 index 00000000..6c3ecc80 --- /dev/null +++ b/test/eor-stream.json @@ -0,0 +1,8 @@ +{"type":"ris_message","data":{"timestamp":1553627987.89,"peer":"72.22.223.9","peer_asn":"11708","id":"00-empty-ipv4-update","raw":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00170200000000","host":"rrc00","type":"UPDATE"}} +{"type":"ris_message","data":{"timestamp":1553627987.89,"peer":"72.22.223.9","peer_asn":"11708","id":"01-eor-mpunreach-ipv6","raw":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF001D0200000006800F03000201","host":"rrc00","type":"UPDATE"}} +{"type":"ris_message","data":{"timestamp":1553627987.89,"peer":"72.22.223.9","peer_asn":"11708","id":"02-eor-mpunreach-ipv4","raw":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF001D0200000006800F03000101","host":"rrc00","type":"UPDATE"}} +{"type":"ris_message","data":{"timestamp":1553627987.89,"peer":"72.22.223.9","peer_asn":"11708","id":"03-withdrawal-ipv4","raw":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0019020002080A0000","host":"rrc00","type":"UPDATE"}} +{"type":"ris_message","data":{"timestamp":1553627987.89,"peer":"72.22.223.9","peer_asn":"11708","id":"04-withdrawal-mpunreach-ipv6","raw":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0022020000000B800F080002012020010DB8","host":"rrc00","type":"UPDATE"}} +{"type":"ris_message","data":{"timestamp":1553627987.89,"peer":"72.22.223.9","peer_asn":"11708","id":"05-mpunreach-nonunicast","raw":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF001D0200000006800F03000202","host":"rrc00","type":"UPDATE"}} +{"type":"ris_message","data":{"timestamp":1553627987.89,"peer":"72.22.223.9","peer_asn":"11708","id":"06-mpunreach-extra-attr","raw":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0021020000000A40010100800F03000201","host":"rrc00","type":"UPDATE"}} +{"type":"ris_message","data":{"timestamp":1553627987.89,"peer":"72.22.223.9","peer_asn":"11708","id":"07-announcement-ipv4","raw":"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF002F02000000144001010040020602010000FDE9400304C000020118C00002","host":"rrc00","type":"UPDATE"}} From 618521dfc152cee1d9a2be57969431da527ce716 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Wed, 1 Jul 2026 15:52:42 -0700 Subject: [PATCH 08/10] Bump version to 2.4.0 --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 3f90b6d6..aa0a5032 100644 --- a/configure.ac +++ b/configure.ac @@ -29,7 +29,7 @@ AC_PREREQ([2.68]) # bgpstream package version m4_define([PKG_MAJOR_VERSION], [2]) -m4_define([PKG_MID_VERSION], [3]) +m4_define([PKG_MID_VERSION], [4]) m4_define([PKG_MINOR_VERSION], [0]) AC_INIT([libbgpstream], PKG_MAJOR_VERSION.PKG_MID_VERSION.PKG_MINOR_VERSION, [bgpstream-info@caida.org]) From b25225a44c526d6d228e340b2dfb8ada61075069 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Wed, 1 Jul 2026 17:08:16 -0700 Subject: [PATCH 09/10] More strict, fix comment --- lib/formats/bgpstream_parsebgp_common.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/formats/bgpstream_parsebgp_common.c b/lib/formats/bgpstream_parsebgp_common.c index 707d9edd..80f15d84 100644 --- a/lib/formats/bgpstream_parsebgp_common.c +++ b/lib/formats/bgpstream_parsebgp_common.c @@ -313,8 +313,7 @@ static int handle_prefix(bgpstream_elem_t *elem, // Detect an RFC 4724 End-of-RIB marker and, if found, populate `elem` as an // END_OF_RIB elem whose prefix encodes the AFI (0.0.0.0/0 for IPv4 unicast, // ::/0 for IPv6 unicast). Returns 1 if an EoR was detected and `elem` was -// populated, 0 otherwise. The caller guarantees the UPDATE carries no -// withdrawn/announced NLRIs before calling this. +// populated, 0 otherwise. static int handle_end_of_rib(bgpstream_elem_t *elem, parsebgp_bgp_update_t *update) { @@ -339,10 +338,11 @@ static int handle_end_of_rib(bgpstream_elem_t *elem, // IPv4/IPv6 unicast EoR: the UPDATE carries exactly one path attribute, an // empty MP_UNREACH_NLRI (zero withdrawn NLRIs) for the IPv4 or IPv6 unicast - // address family. Requiring MP_UNREACH to be the *only* attribute (and the - // AFI/SAFI to be one we recognise) avoids misclassifying malformed UPDATEs or - // non-IPv4/IPv6 families as an EoR. - if (update->path_attrs.attrs_cnt != 1) { + // address family. Per RFC 4724, a valid EoR must not carry any native NLRIs + // either, so reject UPDATEs that contained IPv4 withdrawals or announcements. + if (update->path_attrs.attrs_cnt != 1 || + update->withdrawn_nlris.prefixes_cnt != 0 || + update->announced_nlris.prefixes_cnt != 0) { return 0; } parsebgp_bgp_update_path_attr_t *mp_unreach_attr = From b118b1faf4e79d25c348f1a1861bf15880e77b73 Mon Sep 17 00:00:00 2001 From: Alistair King Date: Wed, 1 Jul 2026 17:15:35 -0700 Subject: [PATCH 10/10] More rigorous test --- test/bgpstream-test-eor.c | 1 + 1 file changed, 1 insertion(+) diff --git a/test/bgpstream-test-eor.c b/test/bgpstream-test-eor.c index 8bd6ea4f..4f38cf04 100644 --- a/test/bgpstream-test-eor.c +++ b/test/bgpstream-test-eor.c @@ -139,6 +139,7 @@ static int test_eor(void) strcmp(buf, exp->prefixes[e]) == 0); e++; } + CHECK_MSG(exp->desc, "no elem error", erc == 0); CHECK_MSG(exp->desc, "elem count", e == exp->n_elems); rcount++; }