diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fddfc5..d86c85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5. When in endpoint mode, `mctpd` now handles to Set Endpoint ID messages, assigning an EID to local interfaces. +6. `mctpd` now handles downstream MCTP bridges, which may request an EID + pool from their Set Endpoint ID response. It will attempt an EID allocation + from the dynamic range, and pass this to the bridge using a subsequent + Allocate Endpoint IDs command. + ## [2.2] - 2025-07-28 ### Fixed diff --git a/docs/mctpd.md b/docs/mctpd.md index b4a5cf0..f60f568 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -126,7 +126,11 @@ busctl call au.com.codeconstruct.MCTP1 \ Similar to SetupEndpoint, but will always assign an EID rather than querying for existing ones. Will return `new = false` when an endpoint is already known to -`mctpd`. +`mctpd`. If the endpoint is an MCTP bridge (indicated by requesting a pool size +in its Set Endpoint ID response), this method attempts to allocate a contiguous +range of EIDs for the bridge's downstream endpoints. If sufficient contiguous +EIDs are not available within the dynamic allocation pool for the network, only +the bridge's own EID will be assigned, and downstream EID allocation will fail. #### `.AssignEndpointStatic`: `ayy` → `yisb` @@ -148,6 +152,14 @@ Like SetupEndpoint but will not assign EIDs, will only query endpoints for a current EID. The `new` return value is set to `false` for an already known endpoint, or `true` when an endpoint's EID is newly discovered. +Because we are not issuing a Set Endpoint ID as part of the LearnEndpoint call, +we do not have any details of the endpoint's bridge pool range. So, +LearnEndpoint is unsuitable for use with bridge endpoints - it cannot provide +the bridge with its own EID pool. `mctpd` will warn if the device type +reports as a bridge. + +Bridge endpoints should be initialised with `AssignEndpoint` instead. + ## Network objects: `/au/com/codeconstruct/networks/` These objects represent MCTP networks which have been added use `mctp link` @@ -215,6 +227,23 @@ busctl call au.com.codeconstruct.MCTP1 \ Removes the MCTP endpoint from `mctpd`, and deletes routes and neighbour entries. +### MCTP bridge interface: `au.com.codeconstruct.MCTP.Bridge1` interface + +MCTP endpoints that are set up as a bridge device (and therefore have an +EID pool allocated to them, for downstream devices) also carry the +`MCTP.Bridge1` interface. This provides details of the allocated EID pool, via +two properties: + +### `.PoolStart`: `y` + +A constant property representing the first EID in the range allocated for +downstream endpoints. + +### `.PoolEnd`: `y` + +A constant property representing the last EID in the range allocated for +downstream endpoints. + ## Configuration `mctpd` reads configuration data from a TOML file, typically `/etc/mctpd.conf`. diff --git a/src/mctp-control-spec.h b/src/mctp-control-spec.h index e3f2f62..7911f71 100644 --- a/src/mctp-control-spec.h +++ b/src/mctp-control-spec.h @@ -183,6 +183,28 @@ struct mctp_ctrl_resp_resolve_endpoint_id { // ... uint8_t physical_address[N] } __attribute__((__packed__)); +typedef enum { + mctp_ctrl_cmd_allocate_eids_alloc_eids = 0, + mctp_ctrl_cmd_allocate_eids_force_alloc = 1, + mctp_ctrl_cmd_allocate_eids_get_alloc_info = 2, + mctp_ctrl_cmd_allocate_eids_reserved = 3 +} mctp_ctrl_cmd_allocate_eids_op; + +struct mctp_ctrl_cmd_allocate_eids { + struct mctp_ctrl_msg_hdr ctrl_hdr; + uint8_t alloc_eid_op; + uint8_t pool_size; + uint8_t start_eid; +} __attribute__((__packed__)); + +struct mctp_ctrl_resp_allocate_eids { + struct mctp_ctrl_msg_hdr ctrl_hdr; + uint8_t completion_code; + uint8_t status; + uint8_t eid_pool_size; + uint8_t eid_set; +} __attribute__((__packed__)); + #define MCTP_CTRL_HDR_MSG_TYPE 0 #define MCTP_CTRL_HDR_FLAG_REQUEST (1 << 7) #define MCTP_CTRL_HDR_FLAG_DGRAM (1 << 6) @@ -257,6 +279,28 @@ struct mctp_ctrl_resp_resolve_endpoint_id { #define MCTP_SET_EID_ACCEPTED 0x0 #define MCTP_SET_EID_REJECTED 0x1 +/* MCTP Get Endpoint ID request and response fields + * See DSP0236 v1.3.0 Table 15. + */ +#define MCTP_GET_EID_EP_TYPE_SHIFT 4 +#define MCTP_GET_EID_EP_TYPE_MASK 0x03 +#define GET_MCTP_GET_EID_EP_TYPE(field) \ + (((field) >> MCTP_GET_EID_EP_TYPE_SHIFT) & MCTP_GET_EID_EP_TYPE_MASK) +#define MCTP_GET_EID_EP_TYPE_EP 0 +#define MCTP_GET_EID_EP_TYPE_BRIDGE 1 + +#define MCTP_GET_EID_EID_TYPE_SHIFT 0 +#define MCTP_GET_EID_EID_TYPE_MASK 0x03 +#define GET_MCTP_GET_EID_EID_TYPE(field) \ + (((field) >> MCTP_GET_EID_EID_TYPE_SHIFT) & MCTP_GET_EID_EID_TYPE_MASK) +#define MCTP_GET_EID_EID_TYPE_DYNAMIC 0 +/* Static EID is supported, may or may not match current */ +#define MCTP_GET_EID_EID_TYPE_STATIC 1 +/* Current eid is the same as static */ +#define MCTP_GET_EID_EID_TYPE_STATIC_SAME 2 +/* Current eid is different from static */ +#define MCTP_GET_EID_EID_TYPE_STATIC_DIFFERENT 3 + #define MCTP_EID_ALLOCATION_STATUS_SHIFT 0x0 #define MCTP_EID_ALLOCATION_STATUS_MASK 0x3 #define SET_MCTP_EID_ALLOCATION_STATUS(status) \ diff --git a/src/mctpd.c b/src/mctpd.c index d3ae63c..4dc5d35 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ #define MCTP_DBUS_PATH_LINKS "/au/com/codeconstruct/mctp1/interfaces" #define CC_MCTP_DBUS_IFACE_BUSOWNER "au.com.codeconstruct.MCTP.BusOwner1" #define CC_MCTP_DBUS_IFACE_ENDPOINT "au.com.codeconstruct.MCTP.Endpoint1" +#define CC_MCTP_DBUS_IFACE_BRIDGE "au.com.codeconstruct.MCTP.Bridge1" #define CC_MCTP_DBUS_IFACE_TESTING "au.com.codeconstruct.MCTPTesting" #define MCTP_DBUS_NAME "au.com.codeconstruct.MCTP1" #define MCTP_DBUS_IFACE_ENDPOINT "xyz.openbmc_project.MCTP.Endpoint" @@ -151,6 +153,7 @@ struct peer { bool published; sd_bus_slot *slot_obmc_endpoint; sd_bus_slot *slot_cc_endpoint; + sd_bus_slot *slot_bridge; sd_bus_slot *slot_uuid; char *path; @@ -185,6 +188,10 @@ struct peer { uint8_t endpoint_type; uint8_t medium_spec; } recovery; + + // Pool size + uint8_t pool_size; + uint8_t pool_start; }; struct ctx { @@ -232,7 +239,7 @@ static int emit_interface_removed(struct link *link); static int emit_net_added(struct ctx *ctx, struct net *net); static int emit_net_removed(struct ctx *ctx, struct net *net); static int add_peer(struct ctx *ctx, const dest_phys *dest, mctp_eid_t eid, - uint32_t net, struct peer **ret_peer); + uint32_t net, struct peer **ret_peer, bool allow_bridged); static int add_peer_from_addr(struct ctx *ctx, const struct sockaddr_mctp_ext *addr, struct peer **ret_peer); @@ -254,9 +261,11 @@ static int del_local_eid(struct ctx *ctx, uint32_t net, int eid); static int add_net(struct ctx *ctx, uint32_t net); static void del_net(struct net *net); static int add_interface(struct ctx *ctx, int ifindex); +static int endpoint_allocate_eids(struct peer *peer); static const sd_bus_vtable bus_endpoint_obmc_vtable[]; static const sd_bus_vtable bus_endpoint_cc_vtable[]; +static const sd_bus_vtable bus_endpoint_bridge[]; static const sd_bus_vtable bus_endpoint_uuid_vtable[]; __attribute__((format(printf, 1, 2))) static void bug_warn(const char *fmt, ...) @@ -1483,9 +1492,14 @@ static int endpoint_query_phys(struct ctx *ctx, const dest_phys *dest, resp_len, resp_addr); } -/* returns -ECONNREFUSED if the endpoint returns failure. */ +/* returns -ECONNREFUSED if the endpoint returns failure. + * + * Returns new EID (in @new_eidp) and the requested pool size (provided in the + * Set Endpoint ID reponse, in @req_pool_size) on success. + */ static int endpoint_send_set_endpoint_id(const struct peer *peer, - mctp_eid_t *new_eidp) + mctp_eid_t *new_eidp, + uint8_t *req_pool_size) { struct sockaddr_mctp_ext addr; struct mctp_ctrl_cmd_set_eid req = { 0 }; @@ -1493,7 +1507,7 @@ static int endpoint_send_set_endpoint_id(const struct peer *peer, int rc; uint8_t *buf = NULL; size_t buf_size; - uint8_t iid, stat, alloc; + uint8_t iid, stat, alloc, pool_size = 0; const dest_phys *dest = &peer->phys; mctp_eid_t new_eid; @@ -1552,21 +1566,47 @@ static int endpoint_send_set_endpoint_id(const struct peer *peer, alloc = resp->status & 0x3; if (alloc != 0) { - // TODO for bridges - warnx("%s requested allocation pool, unimplemented", - dest_phys_tostr(dest)); + pool_size = resp->eid_pool_size; + if (peer->ctx->verbose) { + fprintf(stderr, + "%s requested allocation of pool size = %d\n", + dest_phys_tostr(dest), pool_size); + } } + if (req_pool_size) + *req_pool_size = pool_size; + rc = 0; out: free(buf); return rc; } -/* Returns the newly added peer. - * Error is -EEXISTS if it exists */ +// Checks if given EID belongs to any bridge's pool range +static bool is_eid_in_bridge_pool(const struct net *n, const struct ctx *ctx, + mctp_eid_t eid) +{ + for (int i = ctx->dyn_eid_min; i <= eid; i++) { + const struct peer *peer = n->peers[i]; + if (peer && peer->pool_size > 0) { + if (eid >= peer->pool_start && + eid < peer->pool_start + peer->pool_size) { + return true; + } + i += peer->pool_size; + } + } + return false; +} + +/* Returns the newly added peer. If @allow_bridged is set, we do not conflict + * with EIDs that are within bridge pool allocations. + * + * Error is -EEXISTS if it exists + */ static int add_peer(struct ctx *ctx, const dest_phys *dest, mctp_eid_t eid, - uint32_t net, struct peer **ret_peer) + uint32_t net, struct peer **ret_peer, bool allow_bridged) { struct peer *peer, **tmp; struct net *n; @@ -1585,6 +1625,11 @@ static int add_peer(struct ctx *ctx, const dest_phys *dest, mctp_eid_t eid, *ret_peer = peer; return 0; } + /* In some cases, we want to allow adding a peer that exists within + * a bridged range - typically when the peer is behind that bridge. + */ + if (!allow_bridged && is_eid_in_bridge_pool(n, ctx, eid)) + return -EEXIST; if (ctx->num_peers == MAX_PEER_SIZE) return -ENOMEM; @@ -1627,7 +1672,7 @@ static int add_peer_from_addr(struct ctx *ctx, phys.hwaddr_len = addr->smctp_halen; return add_peer(ctx, &phys, addr->smctp_base.smctp_addr.s_addr, - addr->smctp_base.smctp_network, ret_peer); + addr->smctp_base.smctp_network, ret_peer, true); } static int check_peer_struct(const struct peer *peer, const struct net *n) @@ -1728,6 +1773,7 @@ static void free_peers(struct ctx *ctx) free(peer->path); sd_bus_slot_unref(peer->slot_obmc_endpoint); sd_bus_slot_unref(peer->slot_cc_endpoint); + sd_bus_slot_unref(peer->slot_bridge); sd_bus_slot_unref(peer->slot_uuid); free(peer); } @@ -1797,13 +1843,69 @@ static int peer_set_mtu(struct ctx *ctx, struct peer *peer, uint32_t mtu) return rc; } +struct eid_allocation { + mctp_eid_t start; + unsigned int extent; /* 0 = only the start EID */ +}; + +/* Allocate an unused dynamic EID for a peer, optionally with an associated + * bridge range (of size @bridged_len). + * + * We try to find the first allocation that contains the base EID plus the + * full range. If no space for that exists, we return the largest + * possible range. If the requested range is 0, then the first available + * (single) EID will suit as a match, the returned alloc->extent will be zero. + * + * It is up to the caller to check whether this range is suitable, and + * actually reserve that EID (& range) if so. + * + * returns 0 on success (with @alloc populated), non-zero on failure. + */ +static int allocate_eid(struct ctx *ctx, struct net *net, + unsigned int bridged_len, struct eid_allocation *alloc) +{ + struct eid_allocation cur = { 0 }, best = { 0 }; + mctp_eid_t eid; + + for (eid = ctx->dyn_eid_min; eid <= ctx->dyn_eid_max; eid++) { + if (net->peers[eid]) { + // reset our current candidate allocation + cur.start = 0; + eid += net->peers[eid]->pool_size; + continue; + } + + // start a new candidate allocation + if (!cur.start) + cur.start = eid; + cur.extent = eid - cur.start; + + // if this suits, we're done + if (cur.extent == bridged_len) { + *alloc = cur; + return 0; + } + + if (cur.extent > best.extent || !best.start) + best = cur; + } + + if (best.start) { + *alloc = best; + return 0; + } + + return -1; +} + static int endpoint_assign_eid(struct ctx *ctx, sd_bus_error *berr, const dest_phys *dest, struct peer **ret_peer, - mctp_eid_t static_eid) + mctp_eid_t static_eid, bool assign_bridge) { - mctp_eid_t e, new_eid; + mctp_eid_t new_eid; struct net *n = NULL; struct peer *peer = NULL; + uint8_t req_pool_size; uint32_t net; int rc; @@ -1820,28 +1922,36 @@ static int endpoint_assign_eid(struct ctx *ctx, sd_bus_error *berr, } if (static_eid) { - rc = add_peer(ctx, dest, static_eid, net, &peer); + rc = add_peer(ctx, dest, static_eid, net, &peer, false); if (rc < 0) return rc; new_eid = static_eid; } else { - /* Find an unused dynamic EID */ - for (e = ctx->dyn_eid_min; e <= ctx->dyn_eid_max; e++) { - if (n->peers[e]) - continue; - rc = add_peer(ctx, dest, e, net, &peer); - if (rc < 0) - return rc; - break; - } - if (e > ctx->dyn_eid_max) { - warnx("Ran out of EIDs for net %d, allocating %s", net, - dest_phys_tostr(dest)); + struct eid_allocation alloc; + unsigned int alloc_size = 0; + + if (assign_bridge) + alloc_size = ctx->max_pool_size; + + rc = allocate_eid(ctx, n, alloc_size, &alloc); + if (rc) { + warnx("Cannot allocate any EID (+pool %d) on net %d for %s", + alloc_size, net, dest_phys_tostr(dest)); sd_bus_error_setf(berr, SD_BUS_ERROR_FAILED, "Ran out of EIDs"); return -EADDRNOTAVAIL; } + + new_eid = alloc.start; + + rc = add_peer(ctx, dest, new_eid, net, &peer, false); + if (rc < 0) + return rc; + + peer->pool_size = alloc.extent; + if (peer->pool_size) + peer->pool_start = new_eid + 1; } /* Add a route to the peer prior to assigning it an EID. @@ -1849,7 +1959,7 @@ static int endpoint_assign_eid(struct ctx *ctx, sd_bus_error *berr, * it should be routable. */ add_peer_route(peer); - rc = endpoint_send_set_endpoint_id(peer, &new_eid); + rc = endpoint_send_set_endpoint_id(peer, &new_eid, &req_pool_size); if (rc == -ECONNREFUSED) sd_bus_error_setf( berr, SD_BUS_ERROR_FAILED, @@ -1859,7 +1969,22 @@ static int endpoint_assign_eid(struct ctx *ctx, sd_bus_error *berr, return rc; } + if (req_pool_size > peer->pool_size) { + warnx("EID %d: requested pool size (%d) > pool size available (%d), limiting.", + peer->eid, req_pool_size, peer->pool_size); + } else { + // peer will likely have requested less than the available range + peer->pool_size = req_pool_size; + } + + if (!peer->pool_size) + peer->pool_start = 0; + if (new_eid != peer->eid) { + // avoid allocation for any different EID in response + warnx("Mismatch of requested from received EID, resetting the pool"); + peer->pool_size = 0; + peer->pool_start = 0; rc = change_peer_eid(peer, new_eid); if (rc == -EEXIST) { sd_bus_error_setf( @@ -2023,7 +2148,7 @@ static int get_endpoint_peer(struct ctx *ctx, sd_bus_error *berr, return 0; } /* New endpoint */ - rc = add_peer(ctx, dest, eid, net, &peer); + rc = add_peer(ctx, dest, eid, net, &peer, false); if (rc < 0) return rc; } @@ -2222,10 +2347,14 @@ static int method_setup_endpoint(sd_bus_message *call, void *data, sd_bus_error *berr) { dest_phys desti = { 0 }, *dest = &desti; + uint8_t ep_type, medium_spec; const char *peer_path = NULL; struct link *link = data; struct ctx *ctx = link->ctx; struct peer *peer = NULL; + bool new = true; + mctp_eid_t eid; + uint32_t net; int rc; dest->ifindex = link->ifindex; @@ -2242,38 +2371,89 @@ static int method_setup_endpoint(sd_bus_message *call, void *data, return sd_bus_error_setf(berr, SD_BUS_ERROR_INVALID_ARGS, "Bad physaddr"); + net = mctp_nl_net_byindex(ctx->nl, dest->ifindex); + if (!net) { + rc = -EINVAL; + goto err; + } + /* Get Endpoint ID */ - rc = get_endpoint_peer(ctx, berr, dest, &peer, NULL); - if (rc >= 0 && peer) { - if (ctx->verbose) - fprintf(stderr, - "%s returning from get_endpoint_peer %s\n", - __func__, peer_tostr(peer)); - peer_path = path_from_peer(peer); - if (!peer_path) - goto err; - return sd_bus_reply_method_return(call, "yisb", peer->eid, - peer->net, peer_path, 0); - } else if (rc == -EEXIST) { - // EEXISTS is OK, we will assign a new eid instead. - } else if (rc < 0) { - // Unhandled error, fail. + rc = query_get_endpoint_id(ctx, dest, &eid, &ep_type, &medium_spec); + if (rc) goto err; + + /* does it exist already? */ + peer = find_peer_by_phys(ctx, dest); + + /* we have a few cases: + * + * - no peer, no eid + * --> set up both + * - no peer, eid, not a bridge: + * --> create a peer with the given EID + * - no peer, eid, bridge: + * --> reassign, including bridge + * - peer, eid (but not matching) + * --> change EID if possible, or reassign + * - peer, no eid: + * --> remove peer, reassign + */ + + bool is_bridge = GET_MCTP_GET_EID_EP_TYPE(ep_type) == + MCTP_GET_EID_EP_TYPE_BRIDGE; + + printf("%s: peer %p, eid %d, is_bridge %d ep type %x\n", __func__, peer, + eid, is_bridge, ep_type); + + if (peer) { + /* TODO: we could check the bridge allocation through the + * Get Allocation Information op of Allocate Endpoint IDs, + * and be slightly more accurate with persisting the EID... + */ + if (eid && peer->eid == eid && is_bridge == !!peer->pool_size) { + /* all matching: no action required */ + new = false; + goto out; + } + /* we have some difference in EID / bridge config, remove and + * reassign */ + remove_peer(peer); + peer = NULL; + } + + /* simple allocation: try to use existing EID */ + if (eid && !is_bridge) { + rc = add_peer(ctx, dest, eid, net, &peer, false); + if (rc == -EEXIST) { + /* proposed EID already present on a different peer, + * fall back to assigning */ + } else if (rc < 0) { + goto err; + } else { + peer->endpoint_type = ep_type; + peer->medium_spec = medium_spec; + rc = setup_added_peer(peer); + if (rc) + goto err; + goto out; + } } - /* Set Endpoint ID */ - rc = endpoint_assign_eid(ctx, berr, dest, &peer, 0); + rc = endpoint_assign_eid(ctx, berr, dest, &peer, 0, true); if (rc < 0) goto err; + if (peer->pool_size) + endpoint_allocate_eids(peer); + +out: peer_path = path_from_peer(peer); - if (!peer_path) + if (!peer_path) { + rc = -EPROTO; goto err; - if (ctx->verbose) - fprintf(stderr, "%s returning from endpoint_assign_eid %s\n", - __func__, peer_tostr(peer)); + } return sd_bus_reply_method_return(call, "yisb", peer->eid, peer->net, - peer_path, 1); + peer_path, new); err: set_berr(ctx, rc, berr); @@ -2315,7 +2495,7 @@ static int method_assign_endpoint(sd_bus_message *call, void *data, peer->net, peer_path, 0); } - rc = endpoint_assign_eid(ctx, berr, dest, &peer, 0); + rc = endpoint_assign_eid(ctx, berr, dest, &peer, 0, true); if (rc < 0) goto err; @@ -2323,6 +2503,9 @@ static int method_assign_endpoint(sd_bus_message *call, void *data, if (!peer_path) goto err; + if (peer->pool_size > 0) + endpoint_allocate_eids(peer); + return sd_bus_reply_method_return(call, "yisb", peer->eid, peer->net, peer_path, 1); err: @@ -2387,7 +2570,7 @@ static int method_assign_endpoint_static(sd_bus_message *call, void *data, } } - rc = endpoint_assign_eid(ctx, berr, dest, &peer, eid); + rc = endpoint_assign_eid(ctx, berr, dest, &peer, eid, false); if (rc < 0) { goto err; } @@ -2441,6 +2624,14 @@ static int method_learn_endpoint(sd_bus_message *call, void *data, if (!peer) return sd_bus_reply_method_return(call, "yisb", 0, 0, "", 0); + uint8_t ep_type = GET_MCTP_GET_EID_EP_TYPE(peer->endpoint_type); + if (ep_type == MCTP_GET_EID_EP_TYPE_BRIDGE) { + warnx("LearnEndpoint, eid %d: Get EID response " + "indicated a bridge with existing EID, " + "but no pool is assignable", + peer->eid); + } + peer_path = path_from_peer(peer); if (!peer_path) goto err; @@ -2515,6 +2706,20 @@ static int peer_route_update(struct peer *peer, uint16_t type) return mctp_nl_route_add(peer->ctx->nl, peer->eid, 0, peer->phys.ifindex, NULL, peer->mtu); } else if (type == RTM_DELROUTE) { + if (peer->pool_size > 0) { + int rc = 0; + struct mctp_fq_addr gw_addr = { 0 }; + gw_addr.net = peer->net; + gw_addr.eid = peer->eid; + rc = mctp_nl_route_del(peer->ctx->nl, peer->pool_start, + peer->pool_size - 1, + peer->phys.ifindex, &gw_addr); + if (rc < 0) + warnx("failed to delete route for peer pool eids %d-%d %s", + peer->pool_start, + peer->pool_start + peer->pool_size - 1, + strerror(-rc)); + } return mctp_nl_route_del(peer->ctx->nl, peer->eid, 0, peer->phys.ifindex, NULL); } @@ -2677,6 +2882,8 @@ static int unpublish_peer(struct peer *peer) peer->slot_obmc_endpoint = NULL; sd_bus_slot_unref(peer->slot_cc_endpoint); peer->slot_cc_endpoint = NULL; + sd_bus_slot_unref(peer->slot_bridge); + peer->slot_bridge = NULL; sd_bus_slot_unref(peer->slot_uuid); peer->slot_uuid = NULL; peer->published = false; @@ -2796,11 +3003,12 @@ static int peer_endpoint_recover(sd_event_source *s, uint64_t usec, * after which we immediately return as there's no old peer state left to * maintain. */ - return endpoint_assign_eid(ctx, NULL, &phys, &peer, 0); + return endpoint_assign_eid(ctx, NULL, &phys, &peer, 0, + false); } /* Confirmation of the same device, apply its already allocated EID */ - rc = endpoint_send_set_endpoint_id(peer, &new_eid); + rc = endpoint_send_set_endpoint_id(peer, &new_eid, NULL); if (rc < 0) { goto reschedule; } @@ -2957,7 +3165,7 @@ static int method_net_learn_endpoint(sd_bus_message *call, void *data, return sd_bus_reply_method_return(call, "sb", path_from_peer(peer), false); - rc = add_peer(ctx, &dest, eid, net->net, &peer); + rc = add_peer(ctx, &dest, eid, net->net, &peer, true); if (rc) { warnx("can't add peer: %s", strerror(-rc)); goto err; @@ -3060,6 +3268,28 @@ static int bus_endpoint_get_prop(sd_bus *bus, const char *path, return rc; } +static int bus_bridge_get_prop(sd_bus *bus, const char *path, + const char *interface, const char *property, + sd_bus_message *reply, void *userdata, + sd_bus_error *berr) +{ + struct peer *peer = userdata; + int rc; + + if (strcmp(property, "PoolStart") == 0) { + rc = sd_bus_message_append(reply, "y", peer->pool_start); + } else if (strcmp(property, "PoolEnd") == 0) { + uint8_t pool_end = peer->pool_start + peer->pool_size - 1; + rc = sd_bus_message_append(reply, "y", pool_end); + } else { + warnx("Unknown bridge property '%s' for %s iface %s", property, + path, interface); + rc = -ENOENT; + } + + return rc; +} + static int bus_network_get_prop(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, @@ -3259,6 +3489,21 @@ static const sd_bus_vtable bus_endpoint_cc_vtable[] = { SD_BUS_VTABLE_END }; +static const sd_bus_vtable bus_endpoint_bridge[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("PoolStart", + "y", + bus_bridge_get_prop, + 0, + SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("PoolEnd", + "y", + bus_bridge_get_prop, + 0, + SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_VTABLE_END +}; + static const sd_bus_vtable bus_link_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_WRITABLE_PROPERTY("Role", @@ -3758,7 +4003,7 @@ static int add_local_eid(struct ctx *ctx, uint32_t net, int eid) } } - rc = add_peer(ctx, &local_phys, eid, net, &peer); + rc = add_peer(ctx, &local_phys, eid, net, &peer, false); if (rc < 0) { bug_warn("Error adding local eid %d net %d", eid, net); return rc; @@ -4240,6 +4485,139 @@ static void free_config(struct ctx *ctx) free(ctx->config_filename); } +static int endpoint_send_allocate_endpoint_ids( + struct peer *peer, mctp_eid_t eid_start, uint8_t eid_pool_size, + mctp_ctrl_cmd_allocate_eids_op op, uint8_t *allocated_pool_size, + mctp_eid_t *allocated_pool_start) +{ + struct sockaddr_mctp_ext addr; + struct mctp_ctrl_cmd_allocate_eids req = { 0 }; + struct mctp_ctrl_resp_allocate_eids *resp = NULL; + uint8_t *buf = NULL; + size_t buf_size; + uint8_t iid, stat; + int rc; + + iid = mctp_next_iid(peer->ctx); + mctp_ctrl_msg_hdr_init_req(&req.ctrl_hdr, iid, + MCTP_CTRL_CMD_ALLOCATE_ENDPOINT_IDS); + req.alloc_eid_op = (uint8_t)(op & 0x03); + req.pool_size = eid_pool_size; + req.start_eid = eid_start; + rc = endpoint_query_peer(peer, MCTP_CTRL_HDR_MSG_TYPE, &req, + sizeof(req), &buf, &buf_size, &addr); + if (rc < 0) + goto out; + + rc = mctp_ctrl_validate_response(buf, buf_size, sizeof(*resp), + peer_tostr_short(peer), iid, + MCTP_CTRL_CMD_ALLOCATE_ENDPOINT_IDS); + + if (rc) + goto out; + + resp = (void *)buf; + if (!resp) { + warnx("Invalid response buffer"); + return -ENOMEM; + } + + stat = resp->status & 0x03; + if (stat == 0x00) { + if (peer->ctx->verbose) { + fprintf(stderr, "Allocation accepted\n"); + } + if (resp->eid_pool_size != eid_pool_size || + resp->eid_set != eid_start) { + warnx("Unexpected pool start %d pool size %d", + resp->eid_set, resp->eid_pool_size); + rc = -1; + goto out; + } + *allocated_pool_size = eid_pool_size; + *allocated_pool_start = eid_start; + } else { + if (stat == 0x1) + warnx("Allocation was rejected: already allocated by other bus" + " pool size %d, pool start %d", + resp->eid_pool_size, resp->eid_set); + rc = -1; + goto out; + } + + if (peer->ctx->verbose) { + fprintf(stderr, "Allocated size of %d, starting from EID %d\n", + resp->eid_pool_size, resp->eid_set); + } + +out: + free(buf); + return rc; +} + +static int endpoint_allocate_eids(struct peer *peer) +{ + uint8_t allocated_pool_size = 0; + mctp_eid_t allocated_pool_start = 0; + int rc = 0; + + if (!mctp_eid_is_valid_unicast(peer->pool_start)) { + warnx("Invalid pool start %d", peer->pool_start); + return -1; + } + rc = endpoint_send_allocate_endpoint_ids( + peer, peer->pool_start, peer->pool_size, + mctp_ctrl_cmd_allocate_eids_alloc_eids, &allocated_pool_size, + &allocated_pool_start); + if (rc) { + warnx("Failed to allocate downstream EIDs"); + //reset peer pool + peer->pool_size = 0; + peer->pool_start = 0; + return rc; + } + peer->pool_size = allocated_pool_size; + peer->pool_start = allocated_pool_start; + + if (!peer->pool_size) + return 0; + + // add gateway route for all bridge's downstream eids + struct mctp_fq_addr gw_addr = { 0 }; + gw_addr.net = peer->net; + gw_addr.eid = peer->eid; + rc = mctp_nl_route_add(peer->ctx->nl, peer->pool_start, + peer->pool_size - 1, peer->phys.ifindex, + &gw_addr, peer->mtu); + if (rc < 0 && rc != -EEXIST) { + warnx("Failed to add gateway route for EID %d: %s", gw_addr.eid, + strerror(-rc)); + return rc; + } + + sd_bus_add_object_vtable(peer->ctx->bus, &peer->slot_bridge, peer->path, + CC_MCTP_DBUS_IFACE_BRIDGE, bus_endpoint_bridge, + peer); + rc = sd_bus_emit_interfaces_added(peer->ctx->bus, peer->path, + CC_MCTP_DBUS_IFACE_BRIDGE, NULL); + if (rc < 0) { + warnx("Failed to emit add %s signal for endpoint %d : %s", + CC_MCTP_DBUS_IFACE_BRIDGE, peer->eid, strerror(-rc)); + } + + if (peer->ctx->verbose) { + fprintf(stderr, + "Bridge (eid %d) assigned pool [%d, %d], size %d\n", + peer->eid, peer->pool_start, + peer->pool_start + peer->pool_size - 1, + peer->pool_size); + } + + // TODO: Polling logic for downstream EID + + return 0; +} + int main(int argc, char **argv) { struct ctx ctxi = { 0 }, *ctx = &ctxi; diff --git a/tests/mctpenv/__init__.py b/tests/mctpenv/__init__.py index 8f82bf9..6a082b1 100644 --- a/tests/mctpenv/__init__.py +++ b/tests/mctpenv/__init__.py @@ -303,6 +303,8 @@ def __init__(self, iface, lladdr, ep_uuid = None, eid = 0, types = None): self.eid = eid self.types = types or [0] self.bridged_eps = [] + self.allocated_pool = None # or (start, size) + # keyed by (type, type-specific-instance) self.commands = {} @@ -348,12 +350,20 @@ async def handle_mctp_control(self, sock, addr, data): # Set Endpoint ID (op, eid) = data[2:] self.eid = eid - data = bytes(hdr + [0x00, 0x00, self.eid, 0x00]) + pool_size = len(self.bridged_eps) + alloc_status = 0x00 + # request a pool if we have one + if pool_size: + alloc_status |= 0x01 + data = bytes(hdr + [0x00, alloc_status, self.eid, pool_size]) await sock.send(raddr, data) elif opcode == 2: # Get Endpoint ID - data = bytes(hdr + [0x00, self.eid, 0x00, 0x00]) + ep_type = 0 + if len(self.bridged_eps) > 0: + ep_type = 0x1 << 4 + data = bytes(hdr + [0x00, self.eid, ep_type, 0x00]) await sock.send(raddr, data) elif opcode == 3: @@ -367,6 +377,22 @@ async def handle_mctp_control(self, sock, addr, data): data = bytes(hdr + [0x00, len(types)] + types) await sock.send(raddr, data) + elif opcode == 8: + # Allocate Endpoint IDs + (_, _, _, pool_size, pool_start) = data + alloc_status = 0x00 + if self.allocated_pool is not None: + alloc_status = 0x01 + else: + self.allocated_pool = (pool_start, pool_size) + # Assign sequential EIDs starting from pool_start + for (n, ep) in enumerate(self.bridged_eps[:pool_size]): + ep.eid = self.allocated_pool[0] + n + + data = bytes(hdr + [0x00, alloc_status, + self.allocated_pool[1], self.allocated_pool[0]]) + await sock.send(raddr, data) + else: await sock.send(raddr, bytes(hdr + [0x05])) # unsupported command diff --git a/tests/test_mctpd.py b/tests/test_mctpd.py index c9b2aaa..4a353ab 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -20,6 +20,7 @@ MCTPD_MCTP_P = '/au/com/codeconstruct/mctp1' MCTPD_MCTP_I = 'au.com.codeconstruct.MCTP.BusOwner1' MCTPD_ENDPOINT_I = 'au.com.codeconstruct.MCTP.Endpoint1' +MCTPD_ENDPOINT_BRIDGE_I = 'au.com.codeconstruct.MCTP.Bridge1' DBUS_OBJECT_MANAGER_I = 'org.freedesktop.DBus.ObjectManager' DBUS_PROPERTIES_I = 'org.freedesktop.DBus.Properties' @@ -926,3 +927,328 @@ async def test_config_dyn_eid_range_max(nursery, dbus, sysnet): res = await mctpd.stop_mctpd() assert res == 0 + +""" Test bridge endpoint dynamic EID assignment and downstream +endpoint EID allocation + +Tests that: +- Bridge endpoint can be assigned a dynamic EID +- Downstream endpoints get contiguous EIDs after bridge's own eid +""" +async def test_assign_dynamic_bridge_eid(dbus, mctpd): + iface = mctpd.system.interfaces[0] + mctp = await mctpd_mctp_iface_obj(dbus, iface) + ep = mctpd.network.endpoints[0] + pool_size = 2 + + # Set up bridged endpoints as undiscovered EID 0 + for i in range(pool_size): + br_ep = Endpoint(iface, bytes(), types=[0]) + ep.add_bridged_ep(br_ep) + mctpd.network.add_endpoint(br_ep) + + # dynamic EID assigment for dev1 + (eid, _, path, new) = await mctp.call_assign_endpoint(ep.lladdr) + + assert new + assert ep.allocated_pool == (eid + 1, pool_size) + +""" Test that static allocations are not permitted, if they would conflict +with a bridge pool""" +async def test_bridge_ep_conflict_static(dbus, mctpd): + iface = mctpd.system.interfaces[0] + mctp = await mctpd_mctp_iface_obj(dbus, iface) + ep = mctpd.network.endpoints[0] + n_bridged = 3 + + # add downstream devices + for i in range(n_bridged): + br_ep = Endpoint(iface, bytes()) + ep.add_bridged_ep(br_ep) + + (eid, _, path, new) = await mctp.call_assign_endpoint(ep.lladdr) + assert ep.allocated_pool == (eid + 1, n_bridged) + + # ensure no static assignment can be made from the bridged range + for i in range(n_bridged): + dev = Endpoint(iface, bytes([0x30 + i])) + mctpd.network.add_endpoint(dev) + with pytest.raises(asyncdbus.errors.DBusError): + await mctp.call_assign_endpoint_static(dev.lladdr, ep.eid + 1 + i) + + # ... but we're okay with the EID following + dev = Endpoint(iface, bytes([0x30 + n_bridged])) + mctpd.network.add_endpoint(dev) + static_eid = ep.eid + 1 + n_bridged + (eid, _, _, _) = await mctp.call_assign_endpoint_static( + dev.lladdr, static_eid + ) + + assert eid == static_eid + +""" Test that learnt allocations (ie, pre-assigned device EIDs) are not +permitted, if they would conflict with a bridge pool """ +async def test_bridge_ep_conflict_learn(dbus, mctpd): + iface = mctpd.system.interfaces[0] + mctp = await mctpd_mctp_iface_obj(dbus, iface) + ep = mctpd.network.endpoints[0] + n_bridged = 3 + + # add downstream devices + for i in range(n_bridged): + br_ep = Endpoint(iface, bytes()) + ep.add_bridged_ep(br_ep) + + (eid, _, path, new) = await mctp.call_assign_endpoint(ep.lladdr) + assert ep.allocated_pool == (eid + 1, n_bridged) + + # ensure no learnt assignment can be made from the bridged range + for i in range(n_bridged): + dev = Endpoint(iface, bytes([0x30 + i]), eid=ep.eid + 1 + i) + mctpd.network.add_endpoint(dev) + with pytest.raises(asyncdbus.errors.DBusError): + await mctp.call_learn_endpoint(dev.lladdr) + + # ... but we're okay with the EID following + dev_eid = ep.eid + 1 + n_bridged + dev = Endpoint(iface, bytes([0x30 + n_bridged]), eid=dev_eid) + mctpd.network.add_endpoint(dev) + (eid, _, _, _) = await mctp.call_learn_endpoint(dev.lladdr) + + assert eid == dev_eid + +""" Test that learnt allocations (ie, pre-assigned device EIDs) are not +permitted through SetupEndpoint, if they would conflict with a bridge pool """ +async def test_bridge_ep_conflict_setup(dbus, mctpd): + iface = mctpd.system.interfaces[0] + mctp = await mctpd_mctp_iface_obj(dbus, iface) + ep = mctpd.network.endpoints[0] + n_bridged = 3 + + # add downstream devices + for i in range(n_bridged): + br_ep = Endpoint(iface, bytes()) + ep.add_bridged_ep(br_ep) + + (eid, _, path, new) = await mctp.call_assign_endpoint(ep.lladdr) + assert ep.allocated_pool == (eid + 1, n_bridged) + pool_range = range(ep.allocated_pool[0], ep.allocated_pool[1] + 1) + + # ensure no SetupEndpoint assignment can be made from the bridged range; + # these should get reassigned elsewhere. + for i in range(n_bridged): + dev = Endpoint(iface, bytes([0x30 + i]), eid=ep.eid + 1 + i) + mctpd.network.add_endpoint(dev) + (eid, _, _, _) = await mctp.call_setup_endpoint(dev.lladdr) + assert eid not in pool_range + +""" Test that mctpd will reassign a bridge endpoints (pre-configured) EID +if necessary to satisfy the bridge pool allocation""" +async def test_bridge_setup_reassign(dbus, mctpd): + iface = mctpd.system.interfaces[0] + mctp = await mctpd_mctp_iface_obj(dbus, iface) + + # ep: regular endpoint, will conflict with a bridge pool + ep = mctpd.network.endpoints[0] + static_eid = 10 + (eid, _, _, _) = await mctp.call_assign_endpoint_static( + ep.lladdr, + static_eid + ) + + assert eid == static_eid + + # br: our bridge + conflict_eid = 9 + br = Endpoint(iface, bytes([ep.lladdr[0] + 1]), eid=conflict_eid) + br.add_bridged_ep(Endpoint(iface, bytes())) + mctpd.network.add_endpoint(br) + + (eid, _, _, _) = await mctp.call_setup_endpoint(br.lladdr) + assert eid != conflict_eid + assert br.allocated_pool is not None + assert br.allocated_pool[0] == eid + 1 + +""" Test that we truncate the requested pool size to + the max_pool_size config """ +async def test_assign_dynamic_eid_limited_pool(nursery, dbus, sysnet): + max_pool_size = 1 + config = f""" + [bus-owner] + max_pool_size = {max_pool_size} + """ + + mctpd = MctpdWrapper(dbus, sysnet, config = config) + await mctpd.start_mctpd(nursery) + + iface = mctpd.system.interfaces[0] + ep = mctpd.network.endpoints[0] + mctp = await mctpd_mctp_iface_obj(dbus, iface) + + # Set up bridged endpoints as undiscovered EID 0 + for i in range(0, 2): + br_ep = Endpoint(iface, bytes(), types=[0, 2]) + ep.add_bridged_ep(br_ep) + mctpd.network.add_endpoint(br_ep) + + # dynamic EID assigment for dev1 + (eid, _, path, new) = await mctp.call_assign_endpoint(ep.lladdr) + + assert new + + bridge_obj = await dbus.get_proxy_object(MCTPD_C, path) + props_iface = await bridge_obj.get_interface(DBUS_PROPERTIES_I) + pool_end = await props_iface.call_get(MCTPD_ENDPOINT_BRIDGE_I, "PoolEnd") + pool_size = pool_end.value - eid + assert pool_size == max_pool_size + + res = await mctpd.stop_mctpd() + assert res == 0 + +""" Test that a limited pool is assigned if we run out of space for a full +allocation""" +async def test_bridge_pool_assign_limited(nursery, dbus, sysnet): + (min_dyn_eid, max_dyn_eid) = (8, 13) + config = f""" + [bus-owner] + dynamic_eid_range = [{min_dyn_eid}, {max_dyn_eid}] + """ + + mctpd = MctpdWrapper(dbus, sysnet, config = config) + await mctpd.start_mctpd(nursery) + + iface = mctpd.system.interfaces[0] + ep = mctpd.network.endpoints[0] + mctp = await mctpd_mctp_iface_obj(dbus, iface) + + # Set up bridged endpoints as undiscovered EID 0; three bridged EPs, + # which is larger than the available space + for i in range(0, 3): + br_ep = Endpoint(iface, bytes(), types=[0, 2]) + ep.add_bridged_ep(br_ep) + mctpd.network.add_endpoint(br_ep) + + # consume middle eid from the range to dev2 + dev2 = Endpoint(iface, bytes([0x09])) + mctpd.network.add_endpoint(dev2) + (eid, _, path, new) = await mctp.call_assign_endpoint_static( + dev2.lladdr, + 10 + ) + assert new + + # dynamic EID assigment for dev1 + (eid, _, path, new) = await mctp.call_assign_endpoint(ep.lladdr) + assert new + assert ep.allocated_pool is not None + # we should have the largest range possible; the 8,9-9 range is smaller + # than the 11,12-13 + assert ep.allocated_pool == (12, 2) + + res = await mctpd.stop_mctpd() + assert res == 0 + +"""During Allocate Endpoint ID exchange, return completion code failure +to indicate no pool has been assigned to the bridge""" +async def test_assign_dynamic_eid_allocation_failure(dbus, mctpd): + class BridgeEndpoint(Endpoint): + async def handle_mctp_control(self, sock, src_addr, msg): + flags, opcode = msg[0:2] + if opcode != 0x8: + return await super().handle_mctp_control(sock, src_addr, msg) + dst_addr = MCTPSockAddr.for_ep_resp(self, src_addr, sock.addr_ext) + + msg = bytes([ + flags & 0x1f, # Rsp + 0x08, # opcode: Allocate Endpoint ID + 0x01, # cc: failure + 0x01, # allocation rejected + 0x00, # pool size + 0x00, # pool start + ]) + await sock.send(dst_addr, msg) + + iface = mctpd.system.interfaces[0] + ep = BridgeEndpoint(iface, bytes([0x1e])) + mctpd.network.add_endpoint(ep) + # Set up downstream endpoints as undiscovered EID 0 + for i in range(0, 2): + br_ep = Endpoint(iface, bytes(), types=[0, 2]) + ep.add_bridged_ep(br_ep) + mctpd.network.add_endpoint(br_ep) + mctp = await mctpd_mctp_iface_obj(dbus, iface) + + # dynamic EID assigment for dev1 + (eid, _, path, new) = await mctp.call_assign_endpoint(ep.lladdr) + assert new + # Interface should not be present for failed pool allocation + with pytest.raises(asyncdbus.errors.InterfaceNotFoundError): + bridge_obj = await dbus.get_proxy_object(MCTPD_C, path) + await bridge_obj.get_interface(MCTPD_ENDPOINT_BRIDGE_I) + +""" Test assigning a non-bridge endpoint, when we don't have capacity for +the speculatively-allocated bridge range""" +async def test_assign_without_bridge_range(dbus, sysnet, nursery): + (dyn_eid_min, dyn_eid_max) = (10, 20) + max_pool_size = (dyn_eid_max - dyn_eid_min) + 1 + config = f""" + [bus-owner] + dynamic_eid_range = [{dyn_eid_min}, {dyn_eid_max}] + max_pool_size = {max_pool_size} + """ + + mctpd = MctpdWrapper(dbus, sysnet, config = config) + await mctpd.start_mctpd(nursery) + + iface = mctpd.system.interfaces[0] + ep = mctpd.network.endpoints[0] + + mctp = await mctpd_mctp_iface_obj(dbus, iface) + + (eid, _, _, _) = await mctp.call_assign_endpoint(ep.lladdr) + + assert eid == dyn_eid_min + res = await mctpd.stop_mctpd() + assert res == 0 + +""" Test that we can still allocate a bridge pool even though we may not have +the maximum EID range available. The bridge pool's full allocation is still +possible, since it is smaller than the configured max""" +async def test_bridge_pool_range_limited(dbus, sysnet, nursery): + # configure for: + # 10: bridge A + # 11-13: bridge A pool + # 14: bridge B + # 15-17: bridge B pool + (dyn_eid_min, dyn_eid_max) = (10, 17) + bridge_downstreams = 3 + # max pool size would consume more than half of the range, so bridge B + # cannot be allocated this max + max_pool_size = 5 + config = f""" + [bus-owner] + dynamic_eid_range = [{dyn_eid_min}, {dyn_eid_max}] + max_pool_size = {max_pool_size} + """ + + mctpd = MctpdWrapper(dbus, sysnet, config = config) + await mctpd.start_mctpd(nursery) + + iface = mctpd.system.interfaces[0] + bridges = [ + Endpoint(iface, bytes([0x30])), + Endpoint(iface, bytes([0x31])), + ] + for bridge in bridges: + mctpd.network.add_endpoint(bridge) + for i in range(bridge_downstreams): + bridge.add_bridged_ep(Endpoint(iface, bytes())) + + iface_obj = await mctpd_mctp_iface_obj(dbus, iface) + for bridge in bridges: + (eid, _, _, _) = await iface_obj.call_assign_endpoint(bridge.lladdr) + assert bridge.allocated_pool is not None + assert bridge.allocated_pool[1] == 3 + + res = await mctpd.stop_mctpd() + assert res == 0