On Wed, Nov 16, 2022 at 02:52:59PM +0100, Theo Buehler wrote:
> On Wed, Nov 16, 2022 at 12:47:46PM +0100, Claudio Jeker wrote:
> > ASPA sets are the 2nd thing that will be generated out of rpki (and
> > rpki-client). ASPA is used to further prevent leaks by providing customer
> > provider attestation (and blocking paths which violate these attestations).
> >
> > I started to implement this and while this code does no checks yet it is
> > enough plumbing to parse and "merge" the ASPA sets. Similar to the roa-set
> > an aspa-set in the config will be parsed and passed to the rtr process
> > where it will be merged with the data from RTR sessions.
> > What is missing is all the code needed in the RDE to do the lookups.
> >
> > A aspa-set is defined like this:
> > aspa-set {
> > source-as 1 transit-as { 5 }
> > source-as 2 expires 1668181648 transit-as { 3 4 }
> > source-as 5 transit-as { 1 2 allow inet 7 allow inet6 }
> > }
>
> Adding this to the end of the default bgpd.conf I get
>
> /tmp/bgpd.conf:134: syntax error, expires not allowed
> /tmp/bgpd.conf:136: syntax error
>
> Deleting the expire time, it parses fine. I suspect the duplicate
> aspa_set rules in parse.y don't work as intended.
No this is actually the roa-set code that triggers this. It changes
noexpires to 1 and then after that aspa-set fails.
I changed to code to set noexpires in the origin-set case (where expires
is not allowed) and then set it back to 0 when leaving that node.
> >
> > As usual more than one aspa-set can be used and these sets will all be
> > merged into one table.
> >
> > Since this is already a lot of code to fiddle with various rb trees and
> > doing various realloc games I would like to get this reviewed and
> > committed before adding a lot more code.
> >
> > Once this is in rpki-client can be adjusted to produce an aspa-set in the
> > openbgpd output.
>
> Generally looks good. The bitmask is a bit of a hack, but I guess we
> can live with it.
My plan is to compress the bitmask before sending it over but because of
the memmove()s it is easier to work with bytes initially.
> A few comments and questions below.
See below
> > @@ -586,6 +597,55 @@ roa_set_l : prefixset_item SOURCEAS as4n
> > }
> > ;
> >
> > +aspa_set : ASPASET '{' optnl aspa_set_l optnl '}'
> > + | ASPASET '{' optnl '}'
> > + ;
> > +
> > +aspa_set_l : aspa_set
> > + | aspa_set_l comma aspa_set
> > + ;
> > +
> > +aspa_set : SOURCEAS as4number expires TRANSITAS '{' optnl
>
> aspa_set was already defined a few lines up. Is this legal yacc? As
> mentioned above, it doesn't seem to work.
Magic. I guess it works because the one look-ahead is able to distinguish
the two. Need to fix this though.
> > +static int
> > +merge_aspa_set(uint32_t as, struct aspa_tas_l *tas, time_t expires)
> > +{
> > + struct aspa_set *aspa, needle = { .as = as };
> > + uint32_t i, num, *newtas;
> > + uint8_t *newtasaid = NULL;
> > +
> > + aspa = RB_FIND(aspa_tree, &conf->aspa, &needle);
> > + if (aspa == NULL) {
> > + if ((aspa = calloc(1, sizeof(*aspa))) == NULL) {
> > + yyerror("out of memory");
> > + return -1;
> > + }
> > + aspa->as = as;
> > + RB_INSERT(aspa_tree, &conf->aspa, aspa);
> > + }
> > +
> > + num = aspa->num + tas->num;
>
> Are these numbers guaranteed to be small enough that we don't overflow
> the 32-bit num? Also, it's not clear to me that num > 0 is guaranteed
> (which seems to be assumed in the for loop below).
ASnumers are 32-bit only. So without duplicates no overflow should happen.
Now if one builds a dumb config it would be possible to overflow this.
I think the avage num here will be around 3-4 and the max will be < 1000.
Adding an extra check here may be appropriate.
> > + newtas = recallocarray(aspa->tas, aspa->num, num, sizeof(uint32_t));
> > + if (newtas == NULL) {
> > + yyerror("out of memory");
> > + return -1;
> > + }
> > + if (aspa->tas_aid) {
>
> I don't really understand the reason for this conditional. Are aspa
> sets with all tas->aid == AFI_UNSPEC common enough to warrant such extra
> dances? Doing this recallocarray() unconditionally would allow us to
> remove the calloc() in the somewhat tricky loop below.
I may drop this here and do the compression in the RTR code.
I expect that 99% of all ASPA entries to be AFI_UNSPEC (as in both IPv4
and IPv6). But since this is both done here and in the RTR code it may be
sensible to just compress at the end.
> > + newtasaid = recallocarray(aspa->tas_aid, aspa->num, num, 1);
>
> extra space after =
fixed
> > + if (newtasaid == NULL) {
> > + free(newtas);
> > + yyerror("out of memory");
> > + return -1;
> > + }
> > + }
> > +
> > + /* fill starting at the end since the tas list is reversed */
> > + for (i = num - 1; tas; tas = tas->next) {
>
> I'd move the i-- here:
>
> for (i = num - 1; tas; tas = tas->next, i--) {
Sure.
> > + newtas[i] = tas->as;
> > + if (tas->aid != AFI_UNSPEC) {
>
> typo: AID_UNSPEC?
Yes, result is the same.
> > @@ -274,6 +296,41 @@ rtr_dispatch_imsg_parent(struct imsgbuf
> > fatalx("IMSG_RECONF_ROA_ITEM bad len");
> > roa_insert(&nconf->roa, imsg.data);
> > break;
> > + case IMSG_RECONF_ASPA:
> > + if (imsg.hdr.len - IMSG_HEADER_SIZE !=
> > + offsetof(struct aspa_set, tas))
> > + fatalx("IMSG_RECONF_ASPA bad len");
> > + if (aspa != NULL)
> > + fatalx("unexpected IMSG_RECONF_ASPA");
> > + if ((aspa = calloc(1, sizeof(*aspa))) == NULL)
> > + fatal("aspa alloc");
>
> Worth switching to reallocarray(NULL, ...) to avoid zeroing and immediately
> overwriting all of it?
It is not overwriting all. Only the bytes up to the tas.
memcpy uses offsetof(struct aspa_set, tas) not sizeof(*aspa).
> > + memcpy(aspa, imsg.data, offsetof(struct aspa_set, tas));
> > + break;
> > + case IMSG_RECONF_ASPA_TAS:
> > + if (imsg.hdr.len - IMSG_HEADER_SIZE !=
> > + aspa->num * sizeof(*aspa->tas))
> > + fatalx("IMSG_RECONF_ASPA_TAS bad len");
> > + aspa->tas = calloc(aspa->num, sizeof(*aspa->tas));
>
> ditto
Here reallocarray(NULL, ...) makes sense.
> > + if (aspa->tas == NULL)
> > + fatal("aspa tas alloc");
> > + memcpy(aspa->tas, imsg.data,
> > + aspa->num * sizeof(*aspa->tas));
> > + break;
> > + case IMSG_RECONF_ASPA_TAS_AID:
> > + if (imsg.hdr.len - IMSG_HEADER_SIZE != aspa->num)
> > + fatalx("IMSG_RECONF_ASPA_TAS_AID bad len");
> > + aspa->tas_aid = malloc(aspa->num);
>
> here you use malloc.
>
> > + if (aspa->tas_aid == NULL)
> > + fatal("aspa tas aid alloc");
> > + memcpy(aspa->tas_aid, imsg.data, aspa->num);
> > + break;
> > +static void
> > +asap_set_entry(struct aspa_set *aspa, uint32_t asnum, uint8_t aid)
>
> typo: aspa_set_entry()
Grrrr. I constanly type asap instead of aspa.
> > +{
> > + uint32_t i, num, *newtas;
> > + uint8_t *newtasaid;
> > +
> > + switch (aid) {
> > + case AID_INET:
> > + aid = 0x1;
> > + break;
> > + case AID_INET6:
> > + aid = 0x2;
> > + break;
> > + case AID_UNSPEC:
> > + aid = 0x3;
>
> This is not yet untangled, right? Or did I miss it?
Yeah, up to here the table could have two entries (one with AID_INET and
one with AID_INET6) but we want to combine them into one entry allowing
both.
I do this here so I can use a simple |= to combine equal entries.
> > + break;
> > + default:
> > + fatalx("aspa_set bad AID");
> > + }
> > +
> > + for (i = 0; i < aspa->num; i++) {
> > + if (asnum < aspa->tas[i] || aspa->tas[i] == 0)
> > + break;
> > + if (asnum == aspa->tas[i]) {
> > + aspa->tas_aid[i] |= aid;
> > + return;
> > + }
> > + }
> > +
> > + num = aspa->num + 1;
> > + newtas = recallocarray(aspa->tas, aspa->num, num, sizeof(uint32_t));
> > + newtasaid = recallocarray(aspa->tas_aid, aspa->num, num, 1);
> > + if (newtas == NULL || newtasaid == NULL)
> > + fatal("aspa_set merge");
> > +
> > + if (i < aspa->num) {
> > + memmove(newtas + i + 1, newtas + i,
> > + (aspa->num - i) * sizeof(uint32_t));
> > + memmove(newtasaid + i + 1, newtasaid + i, (aspa->num - i));
> > + }
>
> Phew. looks correct to me.
yeah, not happy. The ASPA spec is overengineered to make the implementors
lives hard.
> > + newtas[i] = asnum;
> > + newtasaid[i] = aid;
> > +
> > + aspa->num = num;
> > + aspa->tas = newtas;
> > + aspa->tas_aid = newtasaid;
> > +}
> > +
> > +static void
> > +rtr_aspa_merge_set(struct aspa_tree *a, struct aspa_set *mergeset)
> > +{
> > + struct aspa_set *aspa, needle = { .as = mergeset->as };
> > + uint32_t i;
> > +
> > + aspa = RB_FIND(aspa_tree, a, &needle);
> > + if (aspa == NULL) {
> > + if ((aspa = calloc(1, sizeof(*aspa))) == NULL)
> > + fatal("aspa insert");
> > + aspa->as = mergeset->as;
> > + RB_INSERT(aspa_tree, a, aspa);
> > + }
> > +
> > + for (i = 0; i < mergeset->num; i++) {
> > + uint8_t aid = AID_UNSPEC;
> > + if (mergeset->tas_aid != NULL)
> > + aid = mergeset->tas_aid[i];
>
> Again, I'm not quite sure what allowing mergeset->tas_aid == NULL buys us.
> We rely on the fact that AFI_UNSPEC == AID_UNSPEC == 0 when not setting
> them in parse.y.
As mentioned above I will do the reduction in the last step.
--
:wq Claudio