From b9f38727a7ba7c9c7e383ade80dbf77086dfce05 Mon Sep 17 00:00:00 2001 From: Job Snijders Date: Sat, 18 Dec 2021 16:35:28 +0100 Subject: RPKI: Add contextual out-of-bound checks in RTR Prefix PDU handler RFC 6810 and RFC 8210 specify that the "Max Length" value MUST NOT be less than the Prefix Length element (underflow). On the other side, overflow of the Max Length element also is possible, it being an 8-bit unsigned integer allows for values larger than 32 or 128. This also implicitly ensures there is no overflow of "Length" value. When a PDU is received where the Max Length field is corrputed, the RTR client (BIRD) should immediately terminate the session, flush all data learned from that cache, and log an error for the operator. Minor changes done by commiter. --- proto/rpki/packets.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/proto/rpki/packets.c b/proto/rpki/packets.c index dd11f997..d246dd50 100644 --- a/proto/rpki/packets.c +++ b/proto/rpki/packets.c @@ -737,6 +737,33 @@ rpki_handle_prefix_pdu(struct rpki_cache *cache, const struct pdu_header *pdu) net_addr_union addr = {}; rpki_prefix_pdu_2_net_addr(pdu, &addr); + if (type == IPV4_PREFIX) + { + if ((addr.roa4.pxlen > addr.roa4.max_pxlen) || + (addr.roa4.max_pxlen > IP4_MAX_PREFIX_LENGTH)) + { + RPKI_WARN(cache->p, "Received corrupt packet from RPKI cache server: invalid pxlen or max_pxlen"); + byte tmp[pdu->len]; + const struct pdu_header *hton_pdu = rpki_pdu_back_to_network_byte_order((void *) tmp, (const void *) pdu); + rpki_send_error_pdu(cache, CORRUPT_DATA, pdu->len, hton_pdu, "Corrupted PDU: invalid pxlen or max_pxlen"); + rpki_cache_change_state(cache, RPKI_CS_ERROR_FATAL); + return RPKI_ERROR; + } + } + else + { + if ((addr.roa6.pxlen > addr.roa6.max_pxlen) || + (addr.roa6.max_pxlen > IP6_MAX_PREFIX_LENGTH)) + { + RPKI_WARN(cache->p, "Received corrupt packet from RPKI cache server: invalid pxlen or max_pxlen"); + byte tmp[pdu->len]; + const struct pdu_header *hton_pdu = rpki_pdu_back_to_network_byte_order((void *) tmp, (const void *) pdu); + rpki_send_error_pdu(cache, CORRUPT_DATA, pdu->len, hton_pdu, "Corrupted PDU: invalid pxlen or max_pxlen"); + rpki_cache_change_state(cache, RPKI_CS_ERROR_FATAL); + return RPKI_ERROR; + } + } + if (cf->ignore_max_length) { if (type == IPV4_PREFIX) -- cgit v1.2.3 From a39cd2cc0b0c64235457c07e2b618318bbdfcacd Mon Sep 17 00:00:00 2001 From: "Ondrej Zajicek (work)" Date: Mon, 27 Dec 2021 19:10:35 +0100 Subject: BSD: Assume onlink flag on ifaces with only host addresses The BSD kernel does not support the onlink flag and BIRD does not use direct routes for next hop validation, instead depends on interface address ranges. We would like to handle PtMP cases with only host addresses configured, like: ifconfig wg0 192.168.0.10/32 route add 192.168.0.4 -iface wg0 route add 192.168.0.8 -iface wg0 To accept BIRD routes with onlink next-hop, like: route 192.168.42.0/24 via 192.168.0.4%wg0 onlink BIRD would dismiss the route when receiving from the kernel, as the next-hop 192.168.0.4 is not part of any interface subnet and onlink flag is not kept by the BSD kernel. The commit fixes this by assuming that for routes received from the kernel, any next-hop is onlink on ifaces with only host addresses. Thanks to Stefan Haller for the original patch. --- sysdep/bsd/krt-sock.c | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/sysdep/bsd/krt-sock.c b/sysdep/bsd/krt-sock.c index 5c905bc9..521f43f3 100644 --- a/sysdep/bsd/krt-sock.c +++ b/sysdep/bsd/krt-sock.c @@ -366,6 +366,30 @@ krt_replace_rte(struct krt_proto *p, net *n, rte *new, rte *old) } } +/** + * krt_assume_onlink - check if routes on interface are considered onlink + * @iface: The interface of the next hop + * @ipv6: Switch to only consider IPv6 or IPv4 addresses. + * + * The BSD kernel does not support an onlink flag. If the interface has only + * host addresses configured, all routes should be considered as onlink and + * the function returns 1. + */ +static int +krt_assume_onlink(struct iface *iface, int ipv6) +{ + const u8 type = ipv6 ? NET_IP6 : NET_IP4; + + struct ifa *ifa; + WALK_LIST(ifa, iface->addrs) + { + if ((ifa->prefix.type == type) && !(ifa->flags & IA_HOST)) + return 0; + } + + return 1; +} + #define SKIP(ARG...) do { DBG("KRT: Ignoring route - " ARG); return; } while(0) static void @@ -528,15 +552,21 @@ krt_read_route(struct ks_msg *msg, struct krt_proto *p, int scan) a.dest = RTD_UNICAST; if (flags & RTF_GATEWAY) { - neighbor *ng; a.nh.gw = igate; /* Clean up embedded interface ID returned in link-local address */ if (ipa_is_link_local(a.nh.gw)) _I0(a.nh.gw) = 0xfe800000; - ng = neigh_find(&p->p, a.nh.gw, a.nh.iface, 0); - if (!ng || (ng->scope == SCOPE_HOST)) + /* The BSD kernel does not support an onlink flag. We heuristically + set the onlink flag, if the iface has only host addresses. */ + if (krt_assume_onlink(a.nh.iface, ipv6)) + a.nh.flags |= RNF_ONLINK; + + neighbor *nbr; + nbr = neigh_find(&p->p, a.nh.gw, a.nh.iface, + (a.nh.flags & RNF_ONLINK) ? NEF_ONLINK : 0); + if (!nbr || (nbr->scope == SCOPE_HOST)) { /* Ignore routes with next-hop 127.0.0.1, host routes with such next-hop appear on OpenBSD for address aliases. */ -- cgit v1.2.3 From a2a268da4f0c05012d691b57f9b358e3f35ef121 Mon Sep 17 00:00:00 2001 From: Alexander Zubkov Date: Tue, 28 Dec 2021 03:46:13 +0100 Subject: Filter: Add operators to pick community components Add operators that can be used to pick components from pair (standard community) or lc (large community) types. For example: (10, 20).asn --> 10 (10, 20).data --> 20 (10, 20, 30).asn --> 10 (10, 20, 30).data1 --> 20 (10, 20, 30).data2 --> 30 Signed-off-by: Alexander Zubkov --- filter/config.Y | 6 +++++- filter/f-inst.c | 46 +++++++++++++++++++++++++++++++++++++++------- filter/test.conf | 9 +++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/filter/config.Y b/filter/config.Y index 7820e719..2fb55e4e 100644 --- a/filter/config.Y +++ b/filter/config.Y @@ -283,6 +283,7 @@ CF_KEYWORDS(FUNCTION, PRINT, PRINTN, UNSET, RETURN, ROA_CHECK, ASN, SRC, DST, IS_V4, IS_V6, LEN, MAXLEN, + DATA, DATA1, DATA2, DEFINED, ADD, DELETE, CONTAINS, RESET, PREPEND, FIRST, LAST, LAST_NONAGGREGATED, MATCH, @@ -789,13 +790,16 @@ term: | term '.' RD { $$ = f_new_inst(FI_ROUTE_DISTINGUISHER, $1); } | term '.' LEN { $$ = f_new_inst(FI_LENGTH, $1); } | term '.' MAXLEN { $$ = f_new_inst(FI_ROA_MAXLEN, $1); } - | term '.' ASN { $$ = f_new_inst(FI_ROA_ASN, $1); } + | term '.' ASN { $$ = f_new_inst(FI_ASN, $1); } | term '.' SRC { $$ = f_new_inst(FI_NET_SRC, $1); } | term '.' DST { $$ = f_new_inst(FI_NET_DST, $1); } | term '.' MASK '(' term ')' { $$ = f_new_inst(FI_IP_MASK, $1, $5); } | term '.' FIRST { $$ = f_new_inst(FI_AS_PATH_FIRST, $1); } | term '.' LAST { $$ = f_new_inst(FI_AS_PATH_LAST, $1); } | term '.' LAST_NONAGGREGATED { $$ = f_new_inst(FI_AS_PATH_LAST_NAG, $1); } + | term '.' DATA { $$ = f_new_inst(FI_PAIR_DATA, $1); } + | term '.' DATA1 { $$ = f_new_inst(FI_LC_DATA1, $1); } + | term '.' DATA2 { $$ = f_new_inst(FI_LC_DATA2, $1); } /* Communities */ /* This causes one shift/reduce conflict diff --git a/filter/f-inst.c b/filter/f-inst.c index b876a937..7e9e77d2 100644 --- a/filter/f-inst.c +++ b/filter/f-inst.c @@ -910,14 +910,31 @@ ((net_addr_roa6 *) v1.val.net)->max_pxlen); } - INST(FI_ROA_ASN, 1, 1) { /* Get ROA ASN */ - ARG(1, T_NET); - if (!net_is_roa(v1.val.net)) - runtime( "ROA expected" ); + INST(FI_ASN, 1, 1) { /* Get ROA ASN or community ASN part */ + ARG_ANY(1); + RESULT_TYPE(T_INT); + switch(v1.type) + { + case T_NET: + if (!net_is_roa(v1.val.net)) + runtime( "ROA expected" ); - RESULT(T_INT, i, (v1.val.net->type == NET_ROA4) ? - ((net_addr_roa4 *) v1.val.net)->asn : - ((net_addr_roa6 *) v1.val.net)->asn); + RESULT_(T_INT, i, (v1.val.net->type == NET_ROA4) ? + ((net_addr_roa4 *) v1.val.net)->asn : + ((net_addr_roa6 *) v1.val.net)->asn); + break; + + case T_PAIR: + RESULT_(T_INT, i, v1.val.i >> 16); + break; + + case T_LC: + RESULT_(T_INT, i, v1.val.lc.asn); + break; + + default: + runtime( "Net, pair or lc expected" ); + } } INST(FI_IP, 1, 1) { /* Convert prefix to ... */ @@ -951,6 +968,21 @@ RESULT(T_INT, i, as_path_get_last_nonaggregated(v1.val.ad)); } + INST(FI_PAIR_DATA, 1, 1) { /* Get data part from the standard community */ + ARG(1, T_PAIR); + RESULT(T_INT, i, v1.val.i & 0xFFFF); + } + + INST(FI_LC_DATA1, 1, 1) { /* Get data1 part from the large community */ + ARG(1, T_LC); + RESULT(T_INT, i, v1.val.lc.ldp1); + } + + INST(FI_LC_DATA2, 1, 1) { /* Get data2 part from the large community */ + ARG(1, T_LC); + RESULT(T_INT, i, v1.val.lc.ldp2); + } + INST(FI_RETURN, 1, 1) { NEVER_CONSTANT; /* Acquire the return value */ diff --git a/filter/test.conf b/filter/test.conf index 3a8804a1..6f2f6fb5 100644 --- a/filter/test.conf +++ b/filter/test.conf @@ -684,6 +684,11 @@ clist l; clist l2; clist r; { + bt_assert((10, 20).asn = 10); + bt_assert((10, 20).data = 20); + bt_assert(p23.asn = 2); + bt_assert(p23.data = 3); + l = - empty -; bt_assert(l !~ [(*,*)]); bt_assert((l ~ [(*,*)]) != (l !~ [(*,*)])); @@ -940,6 +945,10 @@ lclist r; bt_assert(---empty--- = ---empty---); bt_assert((10, 20, 30) !~ ---empty---); + bt_assert((10, 20, 30).asn = 10); + bt_assert((10, 20, 30).data1 = 20); + bt_assert((10, 20, 30).data2 = 30); + ll = --- empty ---; ll = add(ll, (ten, 20, 30)); ll = add(ll, (1000, 2000, 3000)); -- cgit v1.2.3 From e15e465720c428e765ab88fd587afb4f4f5d70ae Mon Sep 17 00:00:00 2001 From: Alexander Zubkov Date: Tue, 28 Dec 2021 03:48:42 +0100 Subject: Doc: Document community components access operators --- doc/bird.sgml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/bird.sgml b/doc/bird.sgml index 4e4804de..d1059cb1 100644 --- a/doc/bird.sgml +++ b/doc/bird.sgml @@ -1292,6 +1292,9 @@ in the foot). The same syntax can also be used to construct a pair from two arbitrary integer expressions (for example (. + This is a dotted quad of numbers used to represent router IDs (and others). Each component can have a value from 0 to 255. Literals of @@ -1382,6 +1385,10 @@ in the foot). pairs, LCs can be constructed using expressions for its parts, (e.g. (. + Filters recognize four types of sets. Sets are similar to strings: you can pass them around but you can't modify them. Literals of type int -- cgit v1.2.3 From 0e1fd7ea6af8aaeb68a23e320c2a0a0a8480d6de Mon Sep 17 00:00:00 2001 From: Alexander Zubkov Date: Tue, 28 Dec 2021 04:05:05 +0100 Subject: Filter: Add operators to find minimum and maximum element of sets Add operators .min and .max to find minumum or maximum element in sets of types: clist, eclist, lclist. Example usage: bgp_community.min bgp_ext_community.max filter(bgp_large_community, [(as1, as2, *)]).min Signed-off-by: Alexander Zubkov --- filter/config.Y | 3 ++ filter/data.c | 11 +++++ filter/data.h | 2 + filter/f-inst.c | 68 +++++++++++++++++++++++++++++ filter/test.conf | 18 ++++++++ nest/a-set.c | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ nest/attrs.h | 6 +++ 7 files changed, 238 insertions(+) diff --git a/filter/config.Y b/filter/config.Y index 2fb55e4e..8916ea97 100644 --- a/filter/config.Y +++ b/filter/config.Y @@ -287,6 +287,7 @@ CF_KEYWORDS(FUNCTION, PRINT, PRINTN, UNSET, RETURN, DEFINED, ADD, DELETE, CONTAINS, RESET, PREPEND, FIRST, LAST, LAST_NONAGGREGATED, MATCH, + MIN, MAX, EMPTY, FILTER, WHERE, EVAL, ATTRIBUTE, BT_ASSERT, BT_TEST_SUITE, BT_CHECK_ASSIGN, BT_TEST_SAME, FORMAT) @@ -800,6 +801,8 @@ term: | term '.' DATA { $$ = f_new_inst(FI_PAIR_DATA, $1); } | term '.' DATA1 { $$ = f_new_inst(FI_LC_DATA1, $1); } | term '.' DATA2 { $$ = f_new_inst(FI_LC_DATA2, $1); } + | term '.' MIN { $$ = f_new_inst(FI_MIN, $1); } + | term '.' MAX { $$ = f_new_inst(FI_MAX, $1); } /* Communities */ /* This causes one shift/reduce conflict diff --git a/filter/data.c b/filter/data.c index 7c33d2cb..56c1fb17 100644 --- a/filter/data.c +++ b/filter/data.c @@ -68,6 +68,17 @@ f_type_name(enum f_type t) return "?"; } +enum f_type +f_type_element_type(enum f_type t) +{ + switch(t) { + case T_CLIST: return T_PAIR; + case T_ECLIST: return T_EC; + case T_LCLIST: return T_LC; + default: return T_VOID; + }; +} + const struct f_val f_const_empty_path = { .type = T_PATH, .val.ad = &null_adata, diff --git a/filter/data.h b/filter/data.h index d296776d..bb815c34 100644 --- a/filter/data.h +++ b/filter/data.h @@ -188,6 +188,8 @@ void trie_format(const struct f_trie *t, buffer *buf); const char *f_type_name(enum f_type t); +enum f_type f_type_element_type(enum f_type t); + int val_same(const struct f_val *v1, const struct f_val *v2); int val_compare(const struct f_val *v1, const struct f_val *v2); void val_format(const struct f_val *v, buffer *buf); diff --git a/filter/f-inst.c b/filter/f-inst.c index 7e9e77d2..901d2939 100644 --- a/filter/f-inst.c +++ b/filter/f-inst.c @@ -983,6 +983,74 @@ RESULT(T_INT, i, v1.val.lc.ldp2); } + INST(FI_MIN, 1, 1) { /* Get minimum element from set */ + ARG_ANY(1); + RESULT_TYPE(f_type_element_type(v1.type)); + switch(v1.type) + { + case T_CLIST: + { + u32 val = 0; + int_set_min(v1.val.ad, &val); + RESULT_(T_PAIR, i, val); + } + break; + + case T_ECLIST: + { + u64 val = 0; + ec_set_min(v1.val.ad, &val); + RESULT_(T_EC, ec, val); + } + break; + + case T_LCLIST: + { + lcomm val = { 0, 0, 0 }; + lc_set_min(v1.val.ad, &val); + RESULT_(T_LC, lc, val); + } + break; + + default: + runtime( "Clist or lclist expected" ); + } + } + + INST(FI_MAX, 1, 1) { /* Get maximum element from set */ + ARG_ANY(1); + RESULT_TYPE(f_type_element_type(v1.type)); + switch(v1.type) + { + case T_CLIST: + { + u32 val = 0; + int_set_max(v1.val.ad, &val); + RESULT_(T_PAIR, i, val); + } + break; + + case T_ECLIST: + { + u64 val = 0; + ec_set_max(v1.val.ad, &val); + RESULT_(T_EC, ec, val); + } + break; + + case T_LCLIST: + { + lcomm val = { 0, 0, 0 }; + lc_set_max(v1.val.ad, &val); + RESULT_(T_LC, lc, val); + } + break; + + default: + runtime( "Clist or lclist expected" ); + } + } + INST(FI_RETURN, 1, 1) { NEVER_CONSTANT; /* Acquire the return value */ diff --git a/filter/test.conf b/filter/test.conf index 6f2f6fb5..6a28e4b3 100644 --- a/filter/test.conf +++ b/filter/test.conf @@ -781,6 +781,12 @@ clist r; r = filter(l, [(3,1), (*,2)]); bt_assert(r = add(add(-empty-, (3,1)), (3,2))); bt_assert(format(r) = "(clist (3,1) (3,2))"); + + # minimim & maximum element + r = add(add(add(add(add(-empty-, (2,1)), (1,3)), (2,2)), (3,1)), (2,3)); + bt_assert(format(r) = "(clist (2,1) (1,3) (2,2) (3,1) (2,3))"); + bt_assert(r.min = (1,3)); + bt_assert(r.max = (3,1)); } bt_test_suite(t_clist, "Testing lists of communities"); @@ -886,6 +892,12 @@ eclist r; r = filter(el, [(rt, 10, 1), (rt, 10, 25..30), (ro, 10, 40)]); bt_assert(r = add(add(--empty--, (rt, 10, 1)), (rt, 10, 30))); bt_assert(format(r) = "(eclist (rt, 10, 1) (rt, 10, 30))"); + + # minimim & maximum element + r = add(add(add(add(add(--empty--, (rt, 2, 1)), (rt, 1, 3)), (rt, 2, 2)), (rt, 3, 1)), (rt, 2, 3)); + bt_assert(format(r) = "(eclist (rt, 2, 1) (rt, 1, 3) (rt, 2, 2) (rt, 3, 1) (rt, 2, 3))"); + bt_assert(r.min = (rt, 1, 3)); + bt_assert(r.max = (rt, 3, 1)); } bt_test_suite(t_eclist, "Testing lists of extended communities"); @@ -995,6 +1007,12 @@ lclist r; r = filter(ll, [(5..15, *, *), (20, 15..25, *)]); bt_assert(r = add(add(---empty---, (10, 10, 10)), (20, 20, 20))); bt_assert(format(r) = "(lclist (10, 10, 10) (20, 20, 20))"); + + # minimim & maximum element + r = add(add(add(add(add(---empty---, (2, 3, 3)), (1, 2, 3)), (2, 3, 1)), (3, 1, 2)), (2, 1, 3)); + bt_assert(format(r) = "(lclist (2, 3, 3) (1, 2, 3) (2, 3, 1) (3, 1, 2) (2, 1, 3))"); + bt_assert(r.min = (1, 2, 3)); + bt_assert(r.max = (3, 1, 2)); } bt_test_suite(t_lclist, "Testing lists of large communities"); diff --git a/nest/a-set.c b/nest/a-set.c index 1186eb56..71fbac94 100644 --- a/nest/a-set.c +++ b/nest/a-set.c @@ -516,6 +516,48 @@ int_set_sort(struct linpool *pool, const struct adata *src) return dst; } +int +int_set_min(const struct adata *list, u32 *val) +{ + if (!list) + return 0; + + u32 *l = (u32 *) list->data; + int len = int_set_get_size(list); + int i; + + if (len < 1) + return 0; + + *val = *l++; + for (i = 1; i < len; i++, l++) + if (int_set_cmp(val, l) > 0) + *val = *l; + + return 1; +} + +int +int_set_max(const struct adata *list, u32 *val) +{ + if (!list) + return 0; + + u32 *l = (u32 *) list->data; + int len = int_set_get_size(list); + int i; + + if (len < 1) + return 0; + + *val = *l++; + for (i = 1; i < len; i++, l++) + if (int_set_cmp(val, l) < 0) + *val = *l; + + return 1; +} + static int ec_set_cmp(const void *X, const void *Y) @@ -541,6 +583,50 @@ ec_set_sort_x(struct adata *set) qsort(set->data, set->length / 8, 8, ec_set_cmp); } +int +ec_set_min(const struct adata *list, u64 *val) +{ + if (!list) + return 0; + + u32 *l = int_set_get_data(list); + int len = int_set_get_size(list); + int i; + + if (len < 1) + return 0; + + u32 *res = l; l += 2; + for (i = 2; i < len; i += 2, l += 2) + if (ec_set_cmp(res, l) > 0) + res = l; + + *val = ec_generic(res[0], res[1]); + return 1; +} + +int +ec_set_max(const struct adata *list, u64 *val) +{ + if (!list) + return 0; + + u32 *l = int_set_get_data(list); + int len = int_set_get_size(list); + int i; + + if (len < 1) + return 0; + + u32 *res = l; l += 2; + for (i = 2; i < len; i += 2, l += 2) + if (ec_set_cmp(res, l) < 0) + res = l; + + *val = ec_generic(res[0], res[1]); + return 1; +} + static int lc_set_cmp(const void *X, const void *Y) @@ -563,3 +649,47 @@ lc_set_sort(struct linpool *pool, const struct adata *src) qsort(dst->data, dst->length / LCOMM_LENGTH, LCOMM_LENGTH, lc_set_cmp); return dst; } + +int +lc_set_min(const struct adata *list, lcomm *val) +{ + if (!list) + return 0; + + u32 *l = int_set_get_data(list); + int len = int_set_get_size(list); + int i; + + if (len < 1) + return 0; + + u32 *res = l; l += 3; + for (i = 3; i < len; i += 3, l += 3) + if (lc_set_cmp(res, l) > 0) + res = l; + + *val = (lcomm) { res[0], res[1], res[2] }; + return 1; +} + +int +lc_set_max(const struct adata *list, lcomm *val) +{ + if (!list) + return 0; + + u32 *l = int_set_get_data(list); + int len = int_set_get_size(list); + int i; + + if (len < 1) + return 0; + + u32 *res = l; l += 3; + for (i = 3; i < len; i += 3, l += 3) + if (lc_set_cmp(res, l) < 0) + res = l; + + *val = (lcomm) { res[0], res[1], res[2] }; + return 1; +} diff --git a/nest/attrs.h b/nest/attrs.h index 50da817b..ef2b95e6 100644 --- a/nest/attrs.h +++ b/nest/attrs.h @@ -218,6 +218,12 @@ struct adata *ec_set_del_nontrans(struct linpool *pool, const struct adata *set) struct adata *int_set_sort(struct linpool *pool, const struct adata *src); struct adata *ec_set_sort(struct linpool *pool, const struct adata *src); struct adata *lc_set_sort(struct linpool *pool, const struct adata *src); +int int_set_min(const struct adata *list, u32 *val); +int ec_set_min(const struct adata *list, u64 *val); +int lc_set_min(const struct adata *list, lcomm *val); +int int_set_max(const struct adata *list, u32 *val); +int ec_set_max(const struct adata *list, u64 *val); +int lc_set_max(const struct adata *list, lcomm *val); void ec_set_sort_x(struct adata *set); /* Sort in place */ -- cgit v1.2.3 From 77042292ff1ebeb5451f3a4119c3cbde0629f5b1 Mon Sep 17 00:00:00 2001 From: Alexander Zubkov Date: Tue, 28 Dec 2021 04:09:36 +0100 Subject: Doc: Document min/max operators for lists --- doc/bird.sgml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/bird.sgml b/doc/bird.sgml index d1059cb1..a63493da 100644 --- a/doc/bird.sgml +++ b/doc/bird.sgml @@ -1532,7 +1532,7 @@ in the foot). Clist is similar to a set, except that unlike other sets, it can be modified. The type is used for community list (a set of pairs) and for cluster list (a set of quads). There exist no literals of this type. - There are three special operators on clists: + There are special operators on clists: returns the length of clist if returns the minimum element of clist returns the maximum element of clist