From 6e26d3b2b5efd0a2294f19f8972c9cc9ba465d9b Mon Sep 17 00:00:00 2001 From: Jasmine Cha Date: Fri, 11 Sep 2026 05:13:49 +0000 Subject: [PATCH] mctpd: add autonomous discovery for addressless links Implement autonomous discovery on addressless point-to-point links (Serial and USB), addressing Point 3 of issue #174. Because these transports have no physical addressing requirements, discovery can proceed without an external trigger when operating as Bus Owner. Following the bridge endpoint polling model, periodic probing sends a physical Get Endpoint ID query (retries disabled) via an sd_event timer. On response, the daemon assigns an EID and completes peer setup; on timeout or error, the timer is rescheduled. Active probes and peer routes are torn down on link DOWN, and probing re-arms on recovery failure if the link remains UP. Add 'auto_discovery' (disabled by default) and 'probe_interval_ms' under [bus-owner], with per-interface overrides in [[interface]]. Aligned with bridged endpoints, collision prevention with concurrent external setup relies on application behavior. Assisted-by: Gemini-Next Signed-off-by: Jasmine Cha --- CHANGELOG.md | 5 + conf/mctpd.conf | 14 ++ docs/mctpd.md | 39 +++- src/mctpd.c | 276 +++++++++++++++++++++++++- tests/test_mctpd.py | 470 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 801 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2adc2d..ff348dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). To support this, the minimum meson version has been bumped to 0.60, previously 0.59. +3. `mctpd` now supports autonomous discovery on addressless point-to-point + physical links (Serial and USB, per Issue #174 Point 3) operating in Bus + Owner mode. Background periodic probing automatically discovers and assigns + EIDs once the interface is brought UP. + ### Fixes 1. In v2.6. we lost the peer initial MTU sematics, which gave us a safe minimum diff --git a/conf/mctpd.conf b/conf/mctpd.conf index b6f95f10..aa79cd34 100644 --- a/conf/mctpd.conf +++ b/conf/mctpd.conf @@ -20,3 +20,17 @@ max_pool_size = 15 # Bus Owner/bridge polling interval (ms) to check endpoint accessibility. # A value of 0 disables polling. endpoint_poll_ms = 0 + +# Autonomous periodic discovery for point-to-point interfaces with no physical +# addressing requirements (Serial and USB, per Issue #174 Point 3). +# Default is false. +auto_discovery = false + +# Periodic probing interval (ms) for point-to-point interfaces (100 - 60000 ms) +probe_interval_ms = 1000 + +# Example: enable autonomous discovery on serial point-to-point interfaces +# [[interface]] +# match = { phys-type = "serial" } +# auto_discovery = true +# probe_interval_ms = 500 diff --git a/docs/mctpd.md b/docs/mctpd.md index 04e824a0..63c3b4f0 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -434,6 +434,33 @@ Such periodic polling is common for all the briged endpoints among allocated pool space [`.PoolStart` - `.PoolEnd`] of the bridge. Polling could be provisioned to be disabled via setting the value as ```0```. +#### `auto_discovery`: Autonomous discovery on point-to-point links + +* type: boolean +* default: `false` + +Enables autonomous discovery (`Get Endpoint ID` 0x02) for point-to-point links +without physical addressing requirements (Serial and USB, per Issue #174 Point 3) +running in `bus-owner` mode. + +For addressless point-to-point transports (Serial and USB), `mctpd` periodically probes the link +once brought `IFF_UP` until the endpoint responds and an EID is assigned. Probing begins +automatically once the interface is brought administratively `IFF_UP` by system network +configuration (e.g. via `systemd-networkd` or `udev`). + +Similar to bridged endpoint polling, collision prevention with external management +tools (such as manual D-Bus `SetupEndpoint` calls) is not enforced by the daemon; +applications must not concurrently configure endpoints on interfaces managed by +autonomous discovery. + +#### `probe_interval_ms`: Periodic probing interval for point-to-point links + +* type: integer, in milliseconds +* default: 1000 + +Specifies the period between probe attempts on point-to-point links. Valid range is +`100` ms to `60000` ms. + ### `[[interface]]`: per-interface configuration The `[[interface]]` table allows configuration to be applied to specific @@ -444,8 +471,10 @@ Matches are processed in the order they appear in the configuration file; the first `[[interface]]` section that matches is applied. Other content of the interface table is configuration to be applied. The -only setting currently supported is `role`, to set mctpd's role as -either bus-owner or endpoint on this interface. +settings currently supported are: +* `role`: sets mctpd's role as either `bus-owner` or `endpoint` on this interface. +* `auto_discovery`: enables or disables autonomous periodic probing (`true` or `false`). +* `probe_interval_ms`: periodic probing interval in milliseconds (100 - 60000 ms). For example, to apply a `bus-owner` role globally, with interface-specific `endpoint` roles for all i2c devices, and one particular (USB) device: @@ -460,6 +489,12 @@ role = "endpoint" [[interface]] match = { path = "/devices/pci0000:00/0000:00:08.3/usb10/10-0:1.0" } role = "endpoint" + +# Enable auto-discovery on serial links with a faster probe interval: +[[interface]] +match = { phys-type = "serial" } +auto_discovery = true +probe_interval_ms = 500 ``` #### Match types diff --git a/src/mctpd.c b/src/mctpd.c index e74f1183..9682492f 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -67,6 +67,9 @@ static const char *conf_file_default = MCTPD_CONF_FILE_DEFAULT; static const uint64_t max_poll_interval_ms = 10000; static const uint64_t min_poll_interval_ms = 2500; +static const uint64_t default_probe_interval_us = 1000 * 1000; +static const uint64_t max_probe_interval_ms = 60000; +static const uint64_t min_probe_interval_ms = 100; static const mctp_eid_t eid_alloc_min = 0x08; static const mctp_eid_t eid_alloc_max = 0xfe; static const uint8_t MCTP_TYPE_VENDOR_PCIE = 0x7e; @@ -153,6 +156,9 @@ struct link { sd_bus_slot *slot_iface; sd_bus_slot *slot_busowner; sd_event_source *role_defer; + sd_event_source *probe_timer; + bool auto_discovery; + uint64_t probe_interval_us; struct ctx *ctx; }; @@ -266,6 +272,10 @@ struct interface_config { bool role_set; enum endpoint_role role; + + bool auto_discovery_set; + bool auto_discovery; + uint64_t probe_interval_us; }; struct ctx { @@ -316,6 +326,10 @@ struct ctx { // checking endpoint's accessibility. uint64_t endpoint_poll; + // Global autonomous probing default for addressless links + bool auto_discovery; + uint64_t probe_interval_us; + // interface configuration (from config file), to be matched and // applied on new interface events struct interface_config *interface_configs; @@ -378,6 +392,23 @@ static int peer_neigh_update(struct peer *peer, uint16_t type); static int add_interface_local(struct ctx *ctx, int ifindex); static int del_interface(struct link *link); static int rename_interface(struct ctx *ctx, struct link *link, int ifindex); +static void link_start_probe(struct link *link); +static void link_stop_probe(struct link *link); + +/* Point 3 (Issue #174): Link types (serial, USB) that do not have any + * physical addressing requirements and allow discovery with no external trigger. + */ +static inline bool binding_requires_periodic_probe(uint8_t binding) +{ + switch (binding) { + case MCTP_PHYS_BINDING_SERIAL: + case MCTP_PHYS_BINDING_USB: + return true; + default: + return false; + } +} + static int change_net_interface(struct ctx *ctx, int ifindex, uint32_t old_net); static int add_local_eid(struct ctx *ctx, uint32_t net, int eid); static int del_local_eid(struct ctx *ctx, uint32_t net, int eid); @@ -1569,7 +1600,42 @@ static int cb_listen_monitor(sd_event_source *s, int sd, uint32_t revents, } case MCTP_NL_CHANGE_UP: { - // 'up' state is currently unused + struct link *link = c->link_userdata; + if (link) { + bool is_up = + mctp_nl_up_byindex(ctx->nl, c->ifindex); + if (is_up) { + if (binding_requires_periodic_probe( + link->phys_binding) && + link->role == + ENDPOINT_ROLE_BUS_OWNER && + link->auto_discovery) { + link->discovered = + DISCOVERY_UNDISCOVERED; + link_start_probe(link); + } + } else { + link_stop_probe(link); + if (binding_requires_periodic_probe( + link->phys_binding) && + link->role == + ENDPOINT_ROLE_BUS_OWNER && + link->auto_discovery) { + dest_phys dest = { + .ifindex = + link->ifindex, + .hwaddr_len = 0, + }; + struct peer *peer = + find_peer_by_phys( + ctx, &dest); + if (peer) + remove_peer(peer); + link->discovered = + DISCOVERY_UNDISCOVERED; + } + } + } break; } default: @@ -2754,6 +2820,134 @@ static int get_endpoint_peer(struct ctx *ctx, sd_bus_error *berr, return 0; } +static int link_reschedule_probe(sd_event_source *source, uint64_t usec) +{ + int rc = 0; + rc = mctp_ops.sd_event.source_set_time_relative(source, usec); + if (rc >= 0) + rc = sd_event_source_set_enabled(source, SD_EVENT_ONESHOT); + return rc; +} + +static void link_stop_probe(struct link *link) +{ + if (link->probe_timer) { + sd_event_source_disable_unref(link->probe_timer); + link->probe_timer = NULL; + } + if (link->ctx->verbose) { + fprintf(stderr, "Disarmed probe timer for %s\n", + link->path ?: "link"); + } +} + +static int link_probe_timer_cb(sd_event_source *s, uint64_t usec, + void *userdata); + +static void link_start_probe(struct link *link) +{ + struct ctx *ctx = link->ctx; + int rc; + + /* Probing is only valid on presence-blind point-to-point links */ + if (!binding_requires_periodic_probe(link->phys_binding)) + return; + + if (link->role != ENDPOINT_ROLE_BUS_OWNER) + return; + + if (!link->auto_discovery) + return; + + if (link->discovered != DISCOVERY_UNDISCOVERED) + return; + + if (link->probe_timer) + return; + + if (!mctp_nl_up_byindex(ctx->nl, link->ifindex)) + return; + + rc = mctp_ops.sd_event.add_time_relative(ctx->event, &link->probe_timer, + CLOCK_MONOTONIC, + link->probe_interval_us, 0, + link_probe_timer_cb, link); + if (rc < 0) { + warnx("Failed to arm probe timer for %s: %s", + link->path ?: "link", strerror(-rc)); + return; + } + + if (ctx->verbose) { + fprintf(stderr, + "Armed probe timer for %s (interval %" PRIu64 " us)\n", + link->path ?: "link", link->probe_interval_us); + } +} + +/* DSP0236: Periodic probing for addressless point-to-point links. + * + * Like bridged endpoint polling (peer_endpoint_poll), collision prevention + * with external management applications (e.g. concurrent manual SetupEndpoint + * D-Bus calls) is not implemented in the daemon and relies on application + * behavior. + */ +static int link_probe_timer_cb(sd_event_source *s, uint64_t usec, + void *userdata) +{ + struct link *link = userdata; + struct ctx *ctx = link->ctx; + dest_phys dest = { + .ifindex = link->ifindex, + .hwaddr_len = 0, + }; + struct mctp_ctrl_cmd_get_eid req = { 0 }; + struct mctp_ctrl_cmd cmd = { 0 }; + struct peer *peer = NULL; + uint8_t iid; + int rc; + + if (link->discovered != DISCOVERY_UNDISCOVERED) { + link_stop_probe(link); + return 0; + } + + iid = mctp_next_iid(ctx); + mctp_ctrl_msg_hdr_init_req(&req.ctrl_hdr, iid, + MCTP_CTRL_CMD_GET_ENDPOINT_ID); + mctp_ctrl_cmd_init_from_req_type(&cmd, req); + /* don't retry here, we're just probing */ + cmd.disable_retry = true; + + rc = endpoint_query_phys(ctx, &dest, &cmd); + if (rc < 0) { + mctp_ctrl_cmd_free(&cmd); + link_reschedule_probe(s, link->probe_interval_us); + return 0; + } + + mctp_ctrl_cmd_free(&cmd); + + /* Endpoint responded, stop probing and assign EID */ + link_stop_probe(link); + link->discovered = DISCOVERY_DISCOVERED; + + if (ctx->verbose) { + fprintf(stderr, "Probe response on %s: assigning EID\n", + link->path ?: "link"); + } + + rc = endpoint_assign_eid(ctx, NULL, &dest, &peer, 0, false); + if (rc < 0) { + warnx("Failed to assign EID on %s: %s", link->path ?: "link", + strerror(-rc)); + link->discovered = DISCOVERY_UNDISCOVERED; + link_start_probe(link); + } + + return 0; +} + static int query_get_peer_msgtypes(struct peer *peer) { struct mctp_ctrl_resp_get_msg_type_support *resp = NULL; @@ -3863,7 +4057,22 @@ static int peer_endpoint_recover(sd_event_source *s, uint64_t usec, /* Recovery unsuccessful, clean up the peer */ assert(sd_event_source_get_enabled(peer->recovery.source, NULL) == 0); + int ifindex = peer->phys.ifindex; remove_peer(peer); + + /* Re-arm autonomous discovery only when recovery has failed and link is still UP */ + if (ifindex > 0 && mctp_nl_up_byindex(ctx->nl, ifindex)) { + struct link *link = + mctp_nl_get_link_userdata(ctx->nl, ifindex); + if (link && + binding_requires_periodic_probe( + link->phys_binding) && + link->role == ENDPOINT_ROLE_BUS_OWNER && + link->auto_discovery) { + link->discovered = DISCOVERY_UNDISCOVERED; + link_start_probe(link); + } + } } return rc < 0 ? rc : 0; } @@ -4488,6 +4697,16 @@ static int link_set_role(sd_event_source *ev, void *userdata) if (rc) warnx("adding link owner vtable failed: %d", rc); + if (binding_requires_periodic_probe(link->phys_binding) && + link->auto_discovery) { + if (link->discovered == DISCOVERY_UNSUPPORTED) + link->discovered = DISCOVERY_UNDISCOVERED; + if (link->discovered == DISCOVERY_UNDISCOVERED && + mctp_nl_up_byindex(link->ctx->nl, link->ifindex)) { + link_start_probe(link); + } + } + return 0; } @@ -4974,6 +5193,7 @@ static int prune_old_nets(struct ctx *ctx) static void free_link(struct link *link) { + link_stop_probe(link); sd_event_source_disable_unref(link->role_defer); sd_bus_slot_unref(link->slot_iface); sd_bus_slot_unref(link->slot_busowner); @@ -5368,6 +5588,12 @@ static int link_apply_configuration(struct ctx *ctx, struct link *link) if (config->role_set) link->role = config->role; + if (config->auto_discovery_set) + link->auto_discovery = config->auto_discovery; + + if (config->probe_interval_us) + link->probe_interval_us = config->probe_interval_us; + return 0; } @@ -5403,6 +5629,8 @@ static int add_interface(struct ctx *ctx, int ifindex) link->phys_binding = mctp_nl_phys_binding_byindex(ctx->nl, ifindex); /* Use the `role` setting in conf/mctp.conf */ link->role = ctx->default_role; + link->auto_discovery = ctx->auto_discovery; + link->probe_interval_us = ctx->probe_interval_us; link_resolve_sysfs_path(link, ifname); rc = asprintf(&link->path, "%s/%s", MCTP_DBUS_PATH_LINKS, ifname); if (rc < 0) { @@ -5436,6 +5664,10 @@ static int add_interface(struct ctx *ctx, int ifindex) if (link->phys_binding == MCTP_PHYS_BINDING_PCIE_VDM) { link->discovered = DISCOVERY_UNDISCOVERED; + } else if (binding_requires_periodic_probe(link->phys_binding) && + link->role == ENDPOINT_ROLE_BUS_OWNER && + link->auto_discovery) { + link->discovered = DISCOVERY_UNDISCOVERED; } link->published = true; @@ -5444,6 +5676,9 @@ static int add_interface(struct ctx *ctx, int ifindex) link->published = false; } + if (mctp_nl_up_byindex(ctx->nl, ifindex)) + link_start_probe(link); + return rc; err_free: @@ -5704,6 +5939,23 @@ static int parse_config_bus_owner(struct ctx *ctx, toml_table_t *bus_owner) ctx->endpoint_poll = i * 1000; } + val = toml_bool_in(bus_owner, "auto_discovery"); + if (val.ok) + ctx->auto_discovery = val.u.b; + + val = toml_int_in(bus_owner, "probe_interval_ms"); + if (val.ok && val.u.i) { + uint64_t i = val.u.i; + if ((i > max_probe_interval_ms) || + (i < min_probe_interval_ms)) { + warnx("probe interval invalid (%" PRIu64 " - %" PRIu64 + " ms)", + min_probe_interval_ms, max_probe_interval_ms); + return -1; + } + ctx->probe_interval_us = i * 1000; + } + return 0; } @@ -5857,6 +6109,26 @@ static int parse_config_interface(struct ctx *ctx, unsigned int idx, return rc; } + toml_datum_t val = toml_bool_in(interface, "auto_discovery"); + if (val.ok) { + config->auto_discovery_set = true; + config->auto_discovery = val.u.b; + } + + val = toml_int_in(interface, "probe_interval_ms"); + if (val.ok && val.u.i) { + uint64_t i = val.u.i; + if ((i > max_probe_interval_ms) || + (i < min_probe_interval_ms)) { + warnx("interface %u: probe interval invalid (%" PRIu64 + " - %" PRIu64 " ms)", + idx, min_probe_interval_ms, + max_probe_interval_ms); + return -1; + } + config->probe_interval_us = i * 1000; + } + return 0; } @@ -6014,6 +6286,8 @@ static void setup_config_defaults(struct ctx *ctx) ctx->dyn_eid_min = eid_alloc_min; ctx->dyn_eid_max = eid_alloc_max; ctx->endpoint_poll = 0; + ctx->auto_discovery = false; + ctx->probe_interval_us = default_probe_interval_us; } static void free_config(struct ctx *ctx) diff --git a/tests/test_mctpd.py b/tests/test_mctpd.py index 4f75954c..e439eab4 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -2396,3 +2396,473 @@ async def test_iface_config_match_path_none(dbus, sysnet, nursery): res = await mctpd.stop_mctpd() assert res == 0 + + +async def test_auto_discovery_serial_probe(dbus, sysnet, nursery): + """Test autonomous periodic discovery on a serial interface""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + discovered_path = None + + def on_ifaces_added(path, interfaces): + nonlocal discovered_path + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + # Check if already added + objects = await objmgr.call_get_managed_objects() + for path, ifaces in objects.items(): + if MCTPD_ENDPOINT_I in ifaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + break + + if not endpoint_added.is_set(): + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + + assert discovered_path is not None + assert len(mctpd.system.routes) == 1 + assert mctpd.system.routes[0].start_eid == 9 + + iface_obj = await mctpd_mctp_iface_control_obj(dbus, iface) + role = await iface_obj.get_role() + assert role == "BusOwner" + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_disabled(dbus, sysnet, nursery): + """Test that auto_discovery can be disabled globally via [bus-owner]""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = false + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + + def on_ifaces_added(path, interfaces): + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + with trio.move_on_after(0.2) as cancel_scope: + await endpoint_added.wait() + assert cancel_scope.cancelled_caught + assert not endpoint_added.is_set() + + objects = await objmgr.call_get_managed_objects() + remote_eps = [ + p + for p in objects + if p.startswith( + f"/au/com/codeconstruct/mctp1/networks/{iface.net}/endpoints/" + ) + and not p.endswith("/8") + ] + assert len(remote_eps) == 0 + assert len(mctpd.system.routes) == 0 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_default_disabled(dbus, sysnet, nursery): + """Test that auto_discovery is disabled by default without explicit config""" + config = """ + role = "bus-owner" + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + + def on_ifaces_added(path, interfaces): + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + with trio.move_on_after(0.2) as cancel_scope: + await endpoint_added.wait() + assert cancel_scope.cancelled_caught + assert not endpoint_added.is_set() + + objects = await objmgr.call_get_managed_objects() + remote_eps = [ + p + for p in objects + if p.startswith( + f"/au/com/codeconstruct/mctp1/networks/{iface.net}/endpoints/" + ) + and not p.endswith("/8") + ] + assert len(remote_eps) == 0 + assert len(mctpd.system.routes) == 0 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_hotplug_requires_up(dbus, sysnet, nursery): + """Test that newly added point-to-point interface remains quiescent while DOWN, and starts probing only when brought UP""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = False + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + discovered_path = None + + def on_ifaces_added(path, interfaces): + nonlocal discovered_path + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + # While iface.up is False, no remote endpoint should be discovered + with trio.move_on_after(0.2) as cancel_scope: + await endpoint_added.wait() + assert cancel_scope.cancelled_caught + assert not endpoint_added.is_set() + + objects = await objmgr.call_get_managed_objects() + remote_eps = [ + p + for p in objects + if p.startswith( + f"/au/com/codeconstruct/mctp1/networks/{iface.net}/endpoints/" + ) + and not p.endswith("/8") + ] + assert len(remote_eps) == 0 + assert len(mctpd.system.routes) == 0 + + # Bring the interface UP at runtime (simulating 'ip link set mctpserial0 up') + iface.up = True + await mctpd.system.notify_interface(iface) + + # Now probe should trigger and endpoint should be added + if not endpoint_added.is_set(): + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + + assert endpoint_added.is_set() + assert discovered_path is not None + assert len(mctpd.system.routes) == 1 + assert mctpd.system.routes[0].start_eid == 9 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_interface_config_override(dbus, sysnet, nursery): + """Test that [[interface]] config can override global auto_discovery settings""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = false + + [[interface]] + match = { phys-type = "serial" } + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + discovered_path = None + + def on_ifaces_added(path, interfaces): + nonlocal discovered_path + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + assert discovered_path is not None + assert len(mctpd.system.routes) == 1 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_down_up_cycle(dbus, sysnet, nursery): + """Test that interface DOWN removes peer and UP successfully restarts discovery""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + endpoint_removed = trio.Event() + + def on_ifaces_added(path, interfaces): + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + endpoint_added.set() + + def on_ifaces_removed(path, interfaces): + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + endpoint_removed.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + await objmgr.on_interfaces_removed(on_ifaces_removed) + + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + assert len(mctpd.system.routes) == 1 + + # Take link DOWN -> peer and routes must be removed + endpoint_added = trio.Event() + iface.up = False + await mctpd.system.notify_interface(iface) + + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_removed.wait() + assert not cancel_scope.cancelled_caught + assert len(mctpd.system.routes) == 0 + + # Bring link back UP -> probe must resume and peer must re-appear + iface.up = True + await mctpd.system.notify_interface(iface) + + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + assert len(mctpd.system.routes) == 1 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_usb(dbus, sysnet, nursery): + """Test autonomous periodic discovery on a USB point-to-point interface""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.USB + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + discovered_path = None + + def on_ifaces_added(path, interfaces): + nonlocal discovered_path + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + discovered_path = path + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + with trio.move_on_after(2.0) as cancel_scope: + await endpoint_added.wait() + assert not cancel_scope.cancelled_caught + assert discovered_path is not None + assert len(mctpd.system.routes) == 1 + assert mctpd.system.routes[0].start_eid == 9 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_interface_config_override_disable( + dbus, sysnet, nursery +): + """Test that [[interface]] config can override global auto_discovery = true to false""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + + [[interface]] + match = { phys-type = "serial" } + auto_discovery = false + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SERIAL + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + + def on_ifaces_added(path, interfaces): + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + with trio.move_on_after(0.2) as cancel_scope: + await endpoint_added.wait() + assert cancel_scope.cancelled_caught + assert not endpoint_added.is_set() + + objects = await objmgr.call_get_managed_objects() + remote_eps = [ + p + for p in objects + if p.startswith( + f"/au/com/codeconstruct/mctp1/networks/{iface.net}/endpoints/" + ) + and not p.endswith("/8") + ] + assert len(remote_eps) == 0 + assert len(mctpd.system.routes) == 0 + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_auto_discovery_non_point3_transports_not_probed( + dbus, sysnet, nursery +): + """Test that link types other than serial and USB (e.g. KCS) are not probed per Point 3""" + config = """ + role = "bus-owner" + [bus-owner] + auto_discovery = true + probe_interval_ms = 100 + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.KCS + iface.lladdr = b"" + iface.up = True + mctpd.network.endpoints[0].lladdr = b"" + + await mctpd.start_mctpd(nursery) + + mctp_root = await dbus.get_proxy_object(MCTPD_C, MCTPD_MCTP_P) + objmgr = await mctp_root.get_interface(DBUS_OBJECT_MANAGER_I) + + endpoint_added = trio.Event() + + def on_ifaces_added(path, interfaces): + if MCTPD_ENDPOINT_I in interfaces and not path.endswith("/8"): + endpoint_added.set() + + await objmgr.on_interfaces_added(on_ifaces_added) + + with trio.move_on_after(0.5) as cancel_scope: + await endpoint_added.wait() + assert cancel_scope.cancelled_caught + assert not endpoint_added.is_set() + + res = await mctpd.stop_mctpd() + assert res == 0