From 0946fe29a78e9ccd1c440b39662d53bdda19511d Mon Sep 17 00:00:00 2001 From: Jasmine Cha Date: Tue, 1 Sep 2026 04:03:54 +0000 Subject: [PATCH] mctpd: add autonomous periodic discovery for point-to-point links Point-to-point physical links (e.g., Serial, USB, KCS) do not provide presence interrupts when an endpoint attaches or reboots, requiring manual intervention to trigger endpoint discovery. Add autonomous periodic probing for point-to-point interfaces in Bus Owner mode. When an interface is brought UP, mctpd periodically sends Get Endpoint ID requests until the endpoint responds, assigns an EID, and publishes the endpoint on D-Bus. Probing automatically pauses when the link goes DOWN and resumes if the peer is removed. Also add configuration options in mctpd.conf for auto-discovery, probe intervals, and static EID reservations, along with corresponding unit tests. Signed-off-by: Jasmine Cha --- CHANGELOG.md | 6 + conf/mctpd.conf | 24 ++++ docs/mctpd.md | 50 ++++++- src/mctpd.c | 251 ++++++++++++++++++++++++++++++++++- tests/test_mctpd.py | 313 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 638 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2adc2d..073577a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ 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 periodic discovery on point-to-point + physical links (e.g. Serial, USB, KCS) operating in Bus Owner mode. + Background probing automatically discovers and assigns EIDs to endpoints + once the interface is brought UP, with support for per-interface probe + intervals and static EID reservations. + ### 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..1d31e568 100644 --- a/conf/mctpd.conf +++ b/conf/mctpd.conf @@ -20,3 +20,27 @@ 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 addressless interfaces (e.g. Serial, USB, KCS) +# Default is false. +auto_discovery = false + +# Periodic probing interval (ms) for point-to-point interfaces (100 - 60000 ms) +probe_interval_ms = 1000 + +# Interface specific configuration examples: +# +# [[interface]] +# match = { phys-type = "serial" } +# role = "bus-owner" +# auto_discovery = true +# probe_interval_ms = 500 +# static_eid = 8 +# +# [[interface]] +# match = { phys-type = "usb" } +# role = "bus-owner" +# auto_discovery = true +# probe_interval_ms = 500 +# static_eid = 9 + diff --git a/docs/mctpd.md b/docs/mctpd.md index 04e824a0..b46a8625 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -434,6 +434,24 @@ 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 periodic probing on point-to-point links + +* type: boolean +* default: `false` + +Enables autonomous background probing (`Get Endpoint ID` 0x02) for addressless +point-to-point links (e.g. Serial, USB, KCS) running in `bus-owner` mode. When enabled, +`mctpd` automatically probes the link when brought `IFF_UP` until the endpoint +responds, assigns an EID, and transitions the link to `DISCOVERY_DISCOVERED`. + +#### `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 @@ -443,12 +461,34 @@ determines which MCTP interfaces the table applies to. 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. +Supported settings per interface: +* `role`: sets mctpd's role as either `bus-owner` or `endpoint` on this interface. +* `auto_discovery`: overrides global `auto_discovery` for this interface (`true`/`false`). +* `probe_interval_ms`: overrides global probe interval in milliseconds (`100` - `60000`). +* `static_eid`: assigns a fixed static EID (must be in `[8, dynamic_eid_start)`) upon successful probe response, preventing dynamic EID allocation collisions. + +For example, to configure static EIDs and custom probe intervals on point-to-point links: -For example, to apply a `bus-owner` role globally, with interface-specific -`endpoint` roles for all i2c devices, and one particular (USB) device: +```toml +[bus-owner] +dynamic_eid_range = [16, 64] +auto_discovery = true +probe_interval_ms = 1000 + +[[interface]] +match = { phys-type = "serial" } +role = "bus-owner" +auto_discovery = true +probe_interval_ms = 500 +static_eid = 8 + +[[interface]] +match = { phys-type = "usb" } +role = "bus-owner" +auto_discovery = true +probe_interval_ms = 500 +static_eid = 9 +``` ```toml role = "bus-owner" diff --git a/src/mctpd.c b/src/mctpd.c index e74f1183..3f6ed01d 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,10 @@ 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; + mctp_eid_t static_eid; struct ctx *ctx; }; @@ -266,6 +273,11 @@ struct interface_config { bool role_set; enum endpoint_role role; + + bool auto_discovery_set; + bool auto_discovery; + uint64_t probe_interval_us; + mctp_eid_t static_eid; }; struct ctx { @@ -316,6 +328,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 +394,24 @@ 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); + +static inline bool binding_is_point_to_point(uint8_t binding) +{ + switch (binding) { + case MCTP_PHYS_BINDING_SERIAL: + case MCTP_PHYS_BINDING_USB: + case MCTP_PHYS_BINDING_KCS: + case MCTP_PHYS_BINDING_MMBI: + case MCTP_PHYS_BINDING_PCC: + case MCTP_PHYS_BINDING_UCIE: + 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 +1603,16 @@ 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) { + link_start_probe(link); + } else { + link_stop_probe(link); + } + } break; } default: @@ -2214,8 +2257,20 @@ static int remove_peer(struct peer *peer) ctx->peers = NULL; } + int ifindex = peer->phys.ifindex; + free(peer); + if (ifindex > 0) { + struct link *link = mctp_nl_get_link_userdata(ctx->nl, ifindex); + if (link && binding_is_point_to_point(link->phys_binding) && + link->role == ENDPOINT_ROLE_BUS_OWNER && + link->auto_discovery) { + link->discovered = DISCOVERY_UNDISCOVERED; + link_start_probe(link); + } + } + return 0; } @@ -2754,6 +2809,101 @@ static int get_endpoint_peer(struct ctx *ctx, sd_bus_error *berr, return 0; } +static int link_probe_timer_cb(sd_event_source *s, uint64_t usec, + void *userdata); + +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 void link_start_probe(struct link *link) +{ + struct ctx *ctx = link->ctx; + + if (!binding_is_point_to_point(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; + + sd_event_add_time_relative(ctx->event, &link->probe_timer, + CLOCK_MONOTONIC, link->probe_interval_us, 0, + link_probe_timer_cb, link); + if (ctx->verbose) { + fprintf(stderr, + "Armed probe timer for %s (interval %" PRIu64 " us)\n", + link->path ?: "link", link->probe_interval_us); + } +} + +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, + }; + mctp_eid_t ret_eid = 0; + uint8_t ret_ep_type = 0, ret_media_spec = 0; + int rc; + + if (link->discovered != DISCOVERY_UNDISCOVERED) + return 0; + + /* Issue non-retrying Get Endpoint ID request */ + rc = query_get_endpoint_id(ctx, &dest, &ret_eid, &ret_ep_type, + &ret_media_spec, /*peer=*/NULL, + /*retry=*/false); + if (rc == 0) { + struct peer *peer = NULL; + + 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, + link->static_eid, 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; + } + + /* Reschedule next probe tick */ + sd_event_source_set_time_relative(s, link->probe_interval_us); + sd_event_source_set_enabled(s, SD_EVENT_ONESHOT); + return 0; +} + static int query_get_peer_msgtypes(struct peer *peer) { struct mctp_ctrl_resp_get_msg_type_support *resp = NULL; @@ -4974,6 +5124,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); @@ -5361,6 +5512,10 @@ static int link_apply_configuration(struct ctx *ctx, struct link *link) { struct interface_config *config; + link->auto_discovery = ctx->auto_discovery; + link->probe_interval_us = ctx->probe_interval_us; + link->static_eid = 0; + config = link_find_configuration(ctx, link); if (!config) return 0; @@ -5368,6 +5523,14 @@ 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; + + link->static_eid = config->static_eid; + return 0; } @@ -5436,6 +5599,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_is_point_to_point(link->phys_binding) && + link->role == ENDPOINT_ROLE_BUS_OWNER && + link->auto_discovery) { + link->discovered = DISCOVERY_UNDISCOVERED; } link->published = true; @@ -5444,6 +5611,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 +5874,27 @@ 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) + 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 = 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 +6048,49 @@ static int parse_config_interface(struct ctx *ctx, unsigned int idx, return rc; } + conf_str = toml_bool_in(interface, "auto_discovery"); + if (!conf_str.ok) + conf_str = toml_bool_in(interface, "auto-discovery"); + if (conf_str.ok) { + config->auto_discovery_set = true; + config->auto_discovery = conf_str.u.b; + } + + conf_str = toml_int_in(interface, "probe_interval_ms"); + if (!conf_str.ok) + conf_str = toml_int_in(interface, "probe-interval-ms"); + if (conf_str.ok && conf_str.u.i) { + uint64_t i = conf_str.u.i; + if ((i > max_probe_interval_ms) || + (i < min_probe_interval_ms)) { + warnx("interface probe interval invalid (%" PRIu64 + " - %" PRIu64 " ms)", + min_probe_interval_ms, max_probe_interval_ms); + return -1; + } + config->probe_interval_us = i * 1000; + } + + conf_str = toml_int_in(interface, "static_eid"); + if (!conf_str.ok) + conf_str = toml_int_in(interface, "static-eid"); + if (conf_str.ok && conf_str.u.i) { + uint64_t eid = conf_str.u.i; + if (eid < 8 || eid > eid_alloc_max) { + warnx("invalid static_eid %" PRIu64 + " (must be in range [8, %d])", + eid, eid_alloc_max); + return -1; + } + if (ctx->dyn_eid_min > 8 && eid >= ctx->dyn_eid_min) { + warnx("invalid static_eid %" PRIu64 + " (must be in range [8, %d) to avoid dynamic range)", + eid, ctx->dyn_eid_min); + return -1; + } + config->static_eid = eid; + } + return 0; } @@ -5889,6 +6123,17 @@ static int parse_config_interfaces(struct ctx *ctx, toml_array_t *interfaces) rc = parse_config_interface(ctx, i, interface, &configs[i]); if (rc) goto err_free; + + if (configs[i].static_eid) { + for (int j = 0; j < i; j++) { + if (configs[j].static_eid == + configs[i].static_eid) { + warnx("duplicate static_eid %d configured in interfaces array", + configs[i].static_eid); + goto err_free; + } + } + } } ctx->interface_configs = configs; @@ -5960,6 +6205,8 @@ static int parse_config(struct ctx *ctx) } interfaces = toml_array_in(conf_root, "interface"); + if (!interfaces) + interfaces = toml_array_in(conf_root, "interfaces"); if (interfaces) { rc = parse_config_interfaces(ctx, interfaces); if (rc) @@ -6014,6 +6261,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..aef83941 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -2396,3 +2396,316 @@ 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_point_to_point_usb(dbus, sysnet, nursery): + """Test autonomous periodic discovery on 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) + + # 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_static_eid(dbus, sysnet, nursery): + """Test autonomous discovery with static EID configuration and verification""" + config = """ + role = "bus-owner" + [bus-owner] + dynamic_eid_range = [16, 64] + auto_discovery = true + probe_interval_ms = 100 + + [[interface]] + match = { phys-type = "serial" } + role = "bus-owner" + auto_discovery = true + probe_interval_ms = 100 + static_eid = 9 + """ + + 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 + + ep_obj = await mctpd_mctp_endpoint_common_obj(dbus, discovered_path) + assigned_eid = await ep_obj.get_eid() + assert assigned_eid == 9 + + 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 and per-interface""" + 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) + + await trio.sleep(0.2) + + 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) + + await trio.sleep(0.2) + + 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 + await trio.sleep(0.2) + 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 + assert not endpoint_added.is_set() + + # 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