Skip to content

Enforce MD5 auth string invariants via new type#792

Open
taspelund wants to merge 3 commits into
mainfrom
trey/md5-auth-key
Open

Enforce MD5 auth string invariants via new type#792
taspelund wants to merge 3 commits into
mainfrom
trey/md5-auth-key

Conversation

@taspelund

Copy link
Copy Markdown
Contributor

Intoduces Md5AuthString type to wrap a String behind a fallible
constructor and immutable getters. This ensures that an API client is
unable to configure an MD5 auth key that does not comply with the
inputs recommended by RFC 2385 (TCP MD5 option for BGP):

4.5 Key configuration

   It should be noted that the key configuration mechanism of routers
   may restrict the possible keys that may be used between peers.  It is
   strongly recommended that an implementation be able to support at
   minimum a key composed of a string of printable ASCII of 80 bytes or
   less, as this is current practice.

The Illumos and Linux kernel implementations both accept 80-byte keys
with arbitrary contents, so the printable ASCII constraint is purely
coming from RFC 2385 and the need for keys to round-trip through json
via OpenAPI / Dropshot.

Fixes: #765

@taspelund taspelund self-assigned this Jun 16, 2026
@taspelund taspelund added Bug bgp Border Gateway Protocol mgd Maghemite daemon rust Pull requests that update rust code labels Jun 16, 2026
Intoduces Md5AuthString type to wrap a String behind a fallible
constructor and immutable getters. This ensures that an API client is
unable to configure an MD5 auth key that does not comply with the
inputs recommended by RFC 2385 (TCP MD5 option for BGP):
```
4.5 Key configuration

   It should be noted that the key configuration mechanism of routers
   may restrict the possible keys that may be used between peers.  It is
   strongly recommended that an implementation be able to support at
   minimum a key composed of a string of printable ASCII of 80 bytes or
   less, as this is current practice.
```

The Illumos and Linux kernel implementations both accept 80-byte keys
with arbitrary contents, so the printable ASCII constraint is purely
coming from RFC 2385 and the need for keys to round-trip through json
via OpenAPI / Dropshot.

Fixes: #765

Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
@taspelund taspelund force-pushed the trey/md5-auth-key branch from e5f8845 to 7367dc4 Compare June 16, 2026 23:00
@taspelund

Copy link
Copy Markdown
Contributor Author

The libnet dependency is also bumped in this ticket in order to pull in oxidecomputer/netadm-sys#210. This gives us an additional layer of checking around the MD5 auth keys we submit to the kernel.

Comment thread mg-api/src/lib.rs
}]
async fn read_unnumbered_neighbors_v8(
rqctx: RequestContext<Self::Context>,
request: Query<latest::bgp::config::AsnSelector>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? AFAICT latest::bgp::config::AsnSelector is an alias for v1::bgp::config::AsnSelector.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is consistent with the API migration guide, specifically https://github.com/oxidecomputer/dropshot-api-manager/blob/main/guides/new-version.md#for-changed-and-removed-endpoints. Since this endpoint has been removed, its input/output types should never change; they're effectively pinned to whatever latest was at the point at which they were removed.

If latest is still an alias to v1 in this case either will compile and pass the drift checks, but leaving latest in a removed endpoint is a bit of a landmine for future maintainers: if someone changes AsnSelector in the future, they now have to go back to all the API endpoints currently using latest and pin them to the previous-latest version by vN.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _vX suffixed methods exist to handle requests targeting old versions of an API endpoint. Latest is a live target, which means the underlying type could change in a way that affects the behavior of the API handler without causing a compilation failure. I moved these handlers for old versions over to using a pinned version of the types to avoid future issues if this were to change. I believe this should have been done during the RFD 619 migration but was missed due to the size of that change. I didn't bother breaking it out into its own PR because it didn't seem like a large amount of clutter, but I'm happy to do so if you'd prefer that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think John and I were typing at the same time. I would say that where we want to start exercising judgement here is when to retire old API versions.

Afaik, the only consumers of mg-admin-client are nexus, mgadm, falcon-lab and possibly a couple spots in the testbed repo (a4x2 or interop). There's really no reason we need to maintain 10+ API versions when all the clients are under our purview

Comment thread mg-api/src/lib.rs
async fn read_unnumbered_neighbor_v8(
rqctx: RequestContext<Self::Context>,
request: Query<latest::bgp::config::UnnumberedNeighborSelector>,
request: Query<v5::bgp::config::UnnumberedNeighborSelector>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here, why change from the latest alias to the explicitly numbered version?

Comment thread mgd/src/bgp_admin.rs Outdated
.into()
let latest = Neighbor::from_rdb_neighbor_info(rq.asn, &x);
let v11: v11::bgp::config::Neighbor = latest.into();
v8::bgp::config::Neighbor::from(v11).into()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is rather confusing code to read. Constructing a v11 Neighbor to turn into a v8 neighbor to then turn into a v1 neighbor for the return object. It seems like there should be From/Into impls that take care of conversion chains so conversions don't clutter up handler code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general guidance from https://github.com/oxidecomputer/dropshot-api-manager/blob/main/guides/new-version.md#add-conversions-to-or-from-the-immediately-prior-version is to only define a conversion from the most recent version being replaced, but I think based on the end of that section it assumes we're usually only converting 1 step at a time in the API trait:

In general, don't add From impls from other prior versions. (So, if a type changed from v1 to v4 to v9, avoid implementing conversions from v9 to v1 or vice versa.) Instead, hop through intermediate versions in the API trait. In some cases it may be more efficient to have direct conversions to prior versions; use appropriate judgment.

I assume we have this read_neighbors_v1 because converting in the API trait isn't tenable for some reason? So maybe this is a case where appropriate judgment is to define conversions for a longer chain.

@taspelund taspelund Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure RFD 619 says to only impl conversions between the newly introduced type version and the previous version type. The handlers for the legacy API versions would then carry the burden for walking the conversion chain since Rust can't just call .into() and have it implicitly use multiple From/Into conversions in one shot.

Edit: I think John and I were typing at the same time again

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we just implement this handler as something like:

    #[endpoint {
        method = GET,
        path = "/bgp/config/neighbors",
        operation_id = "read_neighbors",
        versions = ..VERSION_MP_BGP,
    }]
    async fn read_neighbors_v1(
        rqctx: RequestContext<Self::Context>,
        request: Query<v1::bgp::config::AsnSelector>,
    ) -> Result<HttpResponseOk<Vec<v1::bgp::config::Neighbor>>, HttpError> {
        Self::read_neighbors_v4(rqctx, request)
            .await
            .map(|r| r.map(|v| v.into_iter().map(Into::into).collect()))
    }

in mg-api/src/lib.rs like the other older versions of this function? Similar for the other _v1 impls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this is harder to parse than the current code, but I don't have strong opinions about the style here. Happy to go this way if it's something you want changed though

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's about more than style, it limits blast radius of endpoint interoperability complexity to just the API trait and From/Into impls that only have to operate across a single version bump.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see what you mean now. I'll push another commit to tidy this up

Comment thread mgd/src/bgp_admin.rs Outdated
Comment thread mgd/src/bgp_admin.rs Outdated
Makes a bunch of the version-suffixed API endpoint handlers into default
methods. Updates the calling convention for all the different versioned
endpoint handlers such that they each call the next latest version and
convert via .into(). Also updates the bgp_apply path to properly walk
the conversion path v1 -> v4 -> v8 instead of skipping v4. Also updates
the bgp_apply group removal test to call do_bgp_apply with latest rather
than using a conversion chain.

Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bgp Border Gateway Protocol Bug mgd Maghemite daemon rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mgd panics on overlong MD5 auth key

3 participants