From 05242c741a5d59a6d41417fae1d4aca0206c4c6f Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Tue, 24 Feb 2026 16:39:12 +0800 Subject: [PATCH 01/11] mctpd: Make parse_config_mode generic Rather than explicityly setting ctx->default_role, allow setting any role pointer. We will use this to parse non-default roles in future. Signed-off-by: Jeremy Kerr --- src/mctpd.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mctpd.c b/src/mctpd.c index 523cda5f..6ea466a5 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -5204,7 +5204,7 @@ static int parse_args(struct ctx *ctx, int argc, char **argv) return 0; } -static int parse_config_mode(struct ctx *ctx, const char *mode) +static int parse_config_mode(const char *mode, enum endpoint_role *rolep) { unsigned int i; @@ -5214,7 +5214,7 @@ static int parse_config_mode(struct ctx *ctx, const char *mode) if (!role->conf_val || strcmp(role->conf_val, mode)) continue; - ctx->default_role = role->role; + *rolep = role->role; return 0; } @@ -5388,7 +5388,7 @@ static int parse_config(struct ctx *ctx) val = toml_string_in(conf_root, "mode"); if (val.ok) { - rc = parse_config_mode(ctx, val.u.s); + rc = parse_config_mode(val.u.s, &ctx->default_role); free(val.u.s); if (rc) goto out_free; From 952c98839c4f9ccd760aa0607659614a3205d0fb Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Thu, 19 Feb 2026 11:30:25 +0800 Subject: [PATCH 02/11] mctpd: Use "role" instead of "mode" Currently, we're flipping between "role" and "mode" for the endpoint / bus-owner settings. Be consistent, so use 'role' everywhere. "mode" is still supported for the config, for backwards compatibility. Signed-off-by: Jeremy Kerr --- CHANGELOG.md | 6 ++++++ conf/mctpd.conf | 4 ++-- docs/mctpd.md | 9 ++++++--- src/mctpd.c | 19 +++++++++++-------- tests/test_mctpd_endpoint.py | 2 +- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ace80a4..6a8ab043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). invalid, having a zeroed control message header. These are now properly populated +### Changed + +1. `mctpd`'s `mode` configuration (setting bus owner vs. endpoint roles) is + now called `role`. Configuration parsing will still allow the `mode` setting, + but this will be deprecated in a later release. + ## [2.5] - 2026-02-17 ### Added diff --git a/conf/mctpd.conf b/conf/mctpd.conf index d505d115..b6f95f10 100644 --- a/conf/mctpd.conf +++ b/conf/mctpd.conf @@ -1,5 +1,5 @@ -# Mode: either bus-owner or endpoint or unknown -mode = "bus-owner" +# Role: either bus-owner or endpoint or unknown +role = "bus-owner" # MCTP protocol configuration. Used for both endpoint and bus-owner modes. [mctp] diff --git a/docs/mctpd.md b/docs/mctpd.md index 4e866e3b..a7b0ef3b 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -315,17 +315,20 @@ The configuration file has a global section, plus function-specific sections. These apply to all modes of `mctpd` operation. One top-level setting is defined: -#### `mode`: mctpd mode of operation +#### `role`: local MCTP device role * type: string enum: `bus-owner` or `endpoint` * default: `bus-owner` -This sets the overall mode of `mctpd`, either as a Bus Owner (`mode = -"bus-owner"`) or Endpoint (`mode = "endpoint"`). In bus owner mode, mctpd will +This sets the overall role of `mctpd`, either as a Bus Owner (`role = +"bus-owner"`) or Endpoint (`role = "endpoint"`). In bus owner mode, mctpd will assume responsibility for allocating addresses to other endpoints. In endpoint mode, mctpd will not allocate addresses, but instead accept allocations from an external bus owner. +Previous versions of `mctpd` used `mode` for this configuration, both `role` +and `mode` are accepted. + ### `[mctp]` section This section affects MCTP protocol behaviour, and any common values used for diff --git a/src/mctpd.c b/src/mctpd.c index 6ea466a5..e27f875e 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -568,13 +568,13 @@ static const char *path_from_peer(const struct peer *peer) return peer->path; } -static int get_role(const char *mode, struct role *role) +static int get_role(const char *role_str, struct role *role) { unsigned int i; for (i = 0; i < ARRAY_SIZE(roles); i++) { if (roles[i].dbus_val && - (strcmp(roles[i].dbus_val, mode) == 0)) { + (strcmp(roles[i].dbus_val, role_str) == 0)) { memcpy(role, &roles[i], sizeof(struct role)); return 0; } @@ -5087,7 +5087,7 @@ static int add_interface(struct ctx *ctx, int ifindex) link->published = false; link->ifindex = ifindex; link->ctx = ctx; - /* Use the `mode` setting in conf/mctp.conf */ + /* Use the `role` setting in conf/mctp.conf */ link->role = ctx->default_role; rc = asprintf(&link->path, "%s/%s", MCTP_DBUS_PATH_LINKS, ifname); if (rc < 0) { @@ -5204,21 +5204,21 @@ static int parse_args(struct ctx *ctx, int argc, char **argv) return 0; } -static int parse_config_mode(const char *mode, enum endpoint_role *rolep) +static int parse_config_role(const char *str, enum endpoint_role *rolep) { unsigned int i; for (i = 0; i < ARRAY_SIZE(roles); i++) { const struct role *role = &roles[i]; - if (!role->conf_val || strcmp(role->conf_val, mode)) + if (!role->conf_val || strcmp(role->conf_val, str)) continue; *rolep = role->role; return 0; } - warnx("invalid value '%s' for mode configuration", mode); + warnx("invalid value '%s' for role configuration", str); return -1; } @@ -5386,9 +5386,12 @@ static int parse_config(struct ctx *ctx) goto out_close; } - val = toml_string_in(conf_root, "mode"); + val = toml_string_in(conf_root, "role"); + if (!val.ok) { + val = toml_string_in(conf_root, "mode"); + } if (val.ok) { - rc = parse_config_mode(val.u.s, &ctx->default_role); + rc = parse_config_role(val.u.s, &ctx->default_role); free(val.u.s); if (rc) goto out_free; diff --git a/tests/test_mctpd_endpoint.py b/tests/test_mctpd_endpoint.py index 0785caeb..f29a967a 100644 --- a/tests/test_mctpd_endpoint.py +++ b/tests/test_mctpd_endpoint.py @@ -24,7 +24,7 @@ @pytest.fixture def config(): return """ - mode = "endpoint" + role = "endpoint" """ From d1f51e23fb899d061531fbb0f7eb92c8cf83ccc4 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Fri, 20 Feb 2026 10:12:57 +0800 Subject: [PATCH 03/11] docs: document role = "unknown" behaviour We added the Unkown state for the dbus interface, but not for the configuration. Add a short paragraph documenting this. Signed-off-by: Jeremy Kerr --- docs/mctpd.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/mctpd.md b/docs/mctpd.md index a7b0ef3b..244dfeb0 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -317,7 +317,7 @@ These apply to all modes of `mctpd` operation. One top-level setting is defined: #### `role`: local MCTP device role -* type: string enum: `bus-owner` or `endpoint` +* type: string enum: `bus-owner`, `endpoint` or `unknown` * default: `bus-owner` This sets the overall role of `mctpd`, either as a Bus Owner (`role = @@ -326,6 +326,10 @@ assume responsibility for allocating addresses to other endpoints. In endpoint mode, mctpd will not allocate addresses, but instead accept allocations from an external bus owner. +A value of `unknown` allows per-interface settings; the dbus interface's +`au.com.codeconstruct.MCTP.Interface1.Role` property may be written to set +a specific role for each interface. + Previous versions of `mctpd` used `mode` for this configuration, both `role` and `mode` are accepted. From 80bc14d20bd0afdb40fb27425df295df19b239d0 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Thu, 19 Feb 2026 15:46:40 +0800 Subject: [PATCH 04/11] tests: mctpenv: allow arbitrary wrapped mctpd args, set in main Add an 'args' argument for MctpdWrapper, allowing arbitrary arguments to be specified. Use that facility in the main() function to pass sys.argv to mctpd. For example: python3 ./tests/mctpenv/__init__.py obj/test-mctpd -c mctpd.conf -v Signed-off-by: Jeremy Kerr --- tests/mctpenv/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/mctpenv/__init__.py b/tests/mctpenv/__init__.py index 1460e700..f2deb245 100644 --- a/tests/mctpenv/__init__.py +++ b/tests/mctpenv/__init__.py @@ -1350,9 +1350,10 @@ async def handle_control(self, nursery): class MctpdWrapper(MctpProcessWrapper): - def __init__(self, bus, sysnet, binary=None, config=None): + def __init__(self, bus, sysnet, binary=None, args=None, config=None): super().__init__(sysnet) self.bus = bus + self.args = args or ['-v'] self.binary = binary or './test-mctpd' self.config = config @@ -1396,18 +1397,18 @@ def name_owner_changed(name, new_owner, old_owner): # start mctpd, passing our control socket env = os.environ.copy() env['MCTP_TEST_SOCK'] = str(self.sock_remote.fileno()) + args = self.args if self.config: config_file = tempfile.NamedTemporaryFile('w', prefix="mctp.conf.") config_file.write(self.config) config_file.flush() - command = [self.binary, '-v', '-c', config_file.name] + args += ['-c', config_file.name] else: config_file = None - command = [self.binary, '-v'] proc = await trio.lowlevel.open_process( - command=command, + command=[self.binary] + args, pass_fds=(1, 2, self.sock_remote.fileno()), env=env, ) @@ -1486,11 +1487,13 @@ async def main(): import asyncdbus binary = None + args = None if len(sys.argv) > 1: binary = sys.argv[1] + args = sys.argv[2:] async with asyncdbus.MessageBus().connect() as dbus: sysnet = await default_sysnet() - mctpd = MctpdWrapper(dbus, sysnet, binary=binary) + mctpd = MctpdWrapper(dbus, sysnet, binary=binary, args=args) async with trio.open_nursery() as nursery: nursery.start_soon(sighandler) await mctpd.start_mctpd(nursery) From 1da633d509e7d3ab3c64cc6b58dc40644b66c3b2 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Fri, 20 Feb 2026 08:23:10 +0800 Subject: [PATCH 05/11] mctpd: store phys binding type on link object We'll want to use it for upcoming link configuration. Signed-off-by: Jeremy Kerr --- src/mctpd.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mctpd.c b/src/mctpd.c index e27f875e..e4597bae 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -140,6 +140,7 @@ struct link { bool published; int ifindex; enum endpoint_role role; + uint8_t phys_binding; char *path; sd_bus_slot *slot_iface; @@ -5077,8 +5078,6 @@ static int add_interface(struct ctx *ctx, int ifindex) return -ENOENT; } - uint8_t phys_binding = mctp_nl_phys_binding_byindex(ctx->nl, ifindex); - struct link *link = calloc(1, sizeof(*link)); if (!link) return -ENOMEM; @@ -5087,6 +5086,7 @@ static int add_interface(struct ctx *ctx, int ifindex) link->published = false; link->ifindex = ifindex; link->ctx = ctx; + link->phys_binding = mctp_nl_phys_binding_byindex(ctx->nl, ifindex); /* Use the `role` setting in conf/mctp.conf */ link->role = ctx->default_role; rc = asprintf(&link->path, "%s/%s", MCTP_DBUS_PATH_LINKS, ifname); @@ -5112,7 +5112,7 @@ static int add_interface(struct ctx *ctx, int ifindex) bus_link_owner_vtable, link); } - if (phys_binding == MCTP_PHYS_BINDING_PCIE_VDM) { + if (link->phys_binding == MCTP_PHYS_BINDING_PCIE_VDM) { link->discovered = DISCOVERY_UNDISCOVERED; } From 70bc3392ca24014076ec4d00637c03e465b49d17 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Tue, 24 Feb 2026 13:48:08 +0800 Subject: [PATCH 06/11] mctpd: Add base interface configuration mechanism We would like to be able to specify configurations that apply to individual interfaces; mainly for splitting Bus Owner vs. Endpoint roles across a mctpd instance. Add base infrastructure for interface-specific configurations, through a `[[interface]]` table in the configuration toml. These interface sections consists of: * a `match` configuration, defining which interfaces the configuration it applies to * the configuration data itself At this point, the only match type we support is "all" (matching all interfaces), and no configuration data is defined. These will be expanded in upcoming changes. Signed-off-by: Jeremy Kerr --- docs/mctpd.md | 22 +++++ src/mctpd.c | 191 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_mctpd.py | 16 ++++ 3 files changed, 229 insertions(+) diff --git a/docs/mctpd.md b/docs/mctpd.md index 244dfeb0..8e553845 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -405,3 +405,25 @@ space. Value should be between [```0.5 * TRECLAIM (5)```- ```10```] seconds. 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```. + +### `[[interface]]`: per-interface configuration + +The `[[interface]]` table allows configuration to be applied to specific +interfaces. Each `[[interface]]` entry contains a "match" definition, which +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. There are +currently no configuration definitions to apply. + +#### Match types + +Match on all interfaces: + +```toml +# match all interfaces +[[interface]] +match = "all" +``` diff --git a/src/mctpd.c b/src/mctpd.c index e4597bae..9712568f 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -244,6 +244,16 @@ struct vdm_type_support { sd_bus_track *source_peer; }; +struct interface_config { + struct interface_config_match { + enum { + IFACE_MATCH_ALL, + } type; + union { + }; + } match; +}; + struct ctx { sd_event *event; sd_bus *bus; @@ -291,6 +301,11 @@ struct ctx { // bus owner/bridge polling interval in usecs for // checking endpoint's accessibility. uint64_t endpoint_poll; + + // interface configuration (from config file), to be matched and + // applied on new interface events + struct interface_config *interface_configs; + size_t num_interface_configs; }; static int emit_endpoint_added(const struct peer *peer); @@ -5062,6 +5077,43 @@ static void del_net(struct net *net) free(net); } +static bool config_link_match(struct interface_config_match *match, + struct link *link) +{ + switch (match->type) { + case IFACE_MATCH_ALL: + return true; + } + return false; +} + +static struct interface_config *link_find_configuration(struct ctx *ctx, + struct link *link) +{ + unsigned int i; + + for (i = 0; i < ctx->num_interface_configs; i++) { + struct interface_config *config = &ctx->interface_configs[i]; + if (config_link_match(&config->match, link)) + return config; + } + + return NULL; +} + +static int link_apply_configuration(struct ctx *ctx, struct link *link) +{ + struct interface_config *config; + + config = link_find_configuration(ctx, link); + if (!config) + return 0; + + // TODO: apply configuration as matched + + return 0; +} + static int add_interface(struct ctx *ctx, int ifindex) { int rc; @@ -5095,6 +5147,13 @@ static int add_interface(struct ctx *ctx, int ifindex) goto err_free; } + rc = link_apply_configuration(ctx, link); + if (rc) { + warnx("Failed to apply link configuration for link index %d", + ifindex); + goto err_free; + } + rc = mctp_nl_set_link_userdata(ctx->nl, ifindex, link); if (rc < 0) { warnx("Failed to set UserData for link index %d", ifindex); @@ -5354,9 +5413,133 @@ static int parse_config_bus_owner(struct ctx *ctx, toml_table_t *bus_owner) return 0; } +enum match_result { + MATCH_RES_NONE, + MATCH_RES_OK, + MATCH_RES_ERR, +}; + +const struct match_parser { + enum match_result (*parse)(toml_table_t *, + struct interface_config_match *); +} match_parsers[] = {}; + +static int parse_config_interface_match(struct ctx *ctx, unsigned int idx, + toml_table_t *interface, + struct interface_config_match *match) +{ + toml_table_t *match_conf; + toml_datum_t match_str; + bool match_set = false; + unsigned int i; + + /* match = "all" is special: no table, but a string */ + match_str = toml_string_in(interface, "match"); + if (match_str.ok) { + char *s = match_str.u.s; + int rc = -1; + + if (!strcmp(s, "all")) { + match->type = IFACE_MATCH_ALL; + rc = 0; + } else { + warnx("invalid interface match value %s", s); + } + + free(s); + return rc; + } + + match_conf = toml_table_in(interface, "match"); + if (!match_conf) { + warnx("no match section for interface index %d", idx); + return -1; + } + +// while match_parsers[] is empty +#pragma GCC diagnostic ignored "-Wtype-limits" + + for (i = 0; i < ARRAY_SIZE(match_parsers); i++) { + const struct match_parser *p = &match_parsers[i]; + enum match_result mr; + + mr = p->parse(match_conf, match); + if (mr == MATCH_RES_ERR) + return -1; + + if (mr == MATCH_RES_OK) { + if (match_set) { + warnx("multiple match types for interface index %d", + idx); + return -1; + } + match_set = true; + } + } + + return match_set ? 0 : -1; +} + +static int parse_config_interface(struct ctx *ctx, unsigned int idx, + toml_table_t *interface, + struct interface_config *config) +{ + int rc; + + rc = parse_config_interface_match(ctx, idx, interface, &config->match); + if (rc) { + warnx("no valid match config for interface index %x", idx); + return -1; + } + + return 0; +} + +static int parse_config_interfaces(struct ctx *ctx, toml_array_t *interfaces) +{ + struct interface_config *configs; + int rc, i, n; + + n = toml_array_nelem(interfaces); + if (n < 0) { + warnx("can't parse interfaces array"); + return -1; + } + if (!n) + return 0; + + configs = calloc(n, sizeof(*configs)); + if (!configs) { + warn("can't allocate %d interface configs", n); + return -1; + } + + for (i = 0; i < n; i++) { + toml_table_t *interface = toml_table_at(interfaces, i); + if (!interface) { + warnx("no interface config at %d?", i); + goto err_free; + } + + rc = parse_config_interface(ctx, i, interface, &configs[i]); + if (rc) + goto err_free; + } + + ctx->interface_configs = configs; + ctx->num_interface_configs = n; + + return 0; + +err_free: + free(configs); + return -1; +} + static int parse_config(struct ctx *ctx) { toml_table_t *conf_root, *mctp_tab, *bus_owner; + toml_array_t *interfaces; bool conf_file_specified; char errbuf[256] = { 0 }; const char *filename; @@ -5411,6 +5594,13 @@ static int parse_config(struct ctx *ctx) goto out_free; } + interfaces = toml_array_in(conf_root, "interface"); + if (interfaces) { + rc = parse_config_interfaces(ctx, interfaces); + if (rc) + goto out_free; + } + rc = 0; out_free: @@ -5464,6 +5654,7 @@ static void setup_config_defaults(struct ctx *ctx) static void free_config(struct ctx *ctx) { free(ctx->config_filename); + free(ctx->interface_configs); } static void free_ctrl_cmd_defaults(struct ctx *ctx) diff --git a/tests/test_mctpd.py b/tests/test_mctpd.py index 4f3d484b..1c8e1bf9 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -4,6 +4,7 @@ from mctp_test_utils import ( mctpd_mctp_iface_obj, + mctpd_mctp_iface_control_obj, mctpd_mctp_network_obj, mctpd_mctp_endpoint_common_obj, mctpd_mctp_endpoint_control_obj, @@ -1980,3 +1981,18 @@ async def handle_mctp_control(self, sock, src_addr, msg): res = await mctpd.stop_mctpd() assert res == 0 + + +async def test_iface_config_none(dbus, sysnet, nursery): + """Test that our interface config tests are functional""" + config = """ + role = "unknown" + """ + mctpd = MctpdWrapper(dbus, sysnet, config=config) + await mctpd.start_mctpd(nursery) + + iface = await mctpd_mctp_iface_control_obj(dbus, mctpd.system.interfaces[0]) + role = await iface.get_role() + assert role == "Unknown" + res = await mctpd.stop_mctpd() + assert res == 0 From 32c337a7d7794d441019ecd4e921b11be9c1f669 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Tue, 24 Feb 2026 14:09:27 +0800 Subject: [PATCH 07/11] mctpd: Apply role configuration via interface sections Allow an interface configuration to set the role on matched interfaces. Signed-off-by: Jeremy Kerr --- docs/mctpd.md | 12 ++++++++++-- src/mctpd.c | 24 +++++++++++++++++++++++- tests/test_mctpd.py | 18 ++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/docs/mctpd.md b/docs/mctpd.md index 8e553845..5a754d14 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -415,8 +415,16 @@ 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. There are -currently no configuration definitions to apply. +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. + +```toml +role = "bus-owner" +[[interface]] +match = ... +role = "endpoint" +``` #### Match types diff --git a/src/mctpd.c b/src/mctpd.c index 9712568f..46fede2b 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -252,6 +252,9 @@ struct interface_config { union { }; } match; + + bool role_set; + enum endpoint_role role; }; struct ctx { @@ -5109,7 +5112,8 @@ static int link_apply_configuration(struct ctx *ctx, struct link *link) if (!config) return 0; - // TODO: apply configuration as matched + if (config->role_set) + link->role = config->role; return 0; } @@ -5484,6 +5488,7 @@ static int parse_config_interface(struct ctx *ctx, unsigned int idx, toml_table_t *interface, struct interface_config *config) { + toml_datum_t conf_str; int rc; rc = parse_config_interface_match(ctx, idx, interface, &config->match); @@ -5492,6 +5497,23 @@ static int parse_config_interface(struct ctx *ctx, unsigned int idx, return -1; } + conf_str = toml_string_in(interface, "role"); + if (conf_str.ok) { + char *s = conf_str.u.s; + int rc = parse_config_role(conf_str.u.s, &config->role); + if (rc) { + warnx("invalid role %s in interface section", s); + } else if (config->role == ENDPOINT_ROLE_UNKNOWN) { + warnx("cannot set 'unknown' role in interface section"); + rc = -1; + } else { + config->role_set = true; + } + free(s); + if (rc) + return rc; + } + return 0; } diff --git a/tests/test_mctpd.py b/tests/test_mctpd.py index 1c8e1bf9..461fbaf5 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -1996,3 +1996,21 @@ async def test_iface_config_none(dbus, sysnet, nursery): assert role == "Unknown" res = await mctpd.stop_mctpd() assert res == 0 + + +async def test_iface_config_match_all(dbus, sysnet, nursery): + """Test that our interface config tests are functional""" + config = """ + role = "unknown" + [[interface]] + match = "all" + role = "bus-owner" + """ + mctpd = MctpdWrapper(dbus, sysnet, config=config) + await mctpd.start_mctpd(nursery) + + iface = await mctpd_mctp_iface_control_obj(dbus, mctpd.system.interfaces[0]) + role = await iface.get_role() + assert role == "BusOwner" + res = await mctpd.stop_mctpd() + assert res == 0 From 5ad2f2581839f0a7c6891f54712d4ef6ff5a79fa Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Tue, 24 Feb 2026 14:55:27 +0800 Subject: [PATCH 08/11] mctpd: Allow interface configuration matches on physical binding type A common use-case is to apply configuration by binding type. For example, having a USB "upstream" link, on which we act as an endpoint, and i2c downstream links, on which we act as a bus owner. Allow interface matches on physical binding types. Signed-off-by: Jeremy Kerr --- docs/mctpd.md | 11 +++++++ src/mctpd.c | 73 ++++++++++++++++++++++++++++++++++++++++++--- tests/test_mctpd.py | 24 +++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/docs/mctpd.md b/docs/mctpd.md index 5a754d14..a3891efc 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -435,3 +435,14 @@ Match on all interfaces: [[interface]] match = "all" ``` + +Match on a physical transport binding type: + +```toml +# match only MCTP-over-i2c interfaces +[[interface]] +match = { phys-type = "i2c" } +``` + +Available binding types are: `SMBus` / `I2C`, `PCIe`, `USB`, `KCS`, `serial`, +`I3C`, `MMBI`, or `UCIE`. Matches are case-insensitive. diff --git a/src/mctpd.c b/src/mctpd.c index 46fede2b..99c6725b 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -248,8 +248,10 @@ struct interface_config { struct interface_config_match { enum { IFACE_MATCH_ALL, + IFACE_MATCH_BINDING, } type; union { + enum mctp_phys_binding binding; }; } match; @@ -5086,6 +5088,8 @@ static bool config_link_match(struct interface_config_match *match, switch (match->type) { case IFACE_MATCH_ALL: return true; + case IFACE_MATCH_BINDING: + return link->phys_binding == match->binding; } return false; } @@ -5285,6 +5289,36 @@ static int parse_config_role(const char *str, enum endpoint_role *rolep) return -1; } +static struct { + const char *name; + enum mctp_phys_binding binding; +} phys_bindings[] = { + { "SMBus", MCTP_PHYS_BINDING_SMBUS }, + { "I2C", MCTP_PHYS_BINDING_SMBUS }, // alias + { "PCIe", MCTP_PHYS_BINDING_PCIE_VDM }, + { "USB", MCTP_PHYS_BINDING_USB }, + { "KCS", MCTP_PHYS_BINDING_KCS }, + { "serial", MCTP_PHYS_BINDING_SERIAL }, + { "I3C", MCTP_PHYS_BINDING_I3C }, + { "MMBI", MCTP_PHYS_BINDING_MMBI }, + { "UCIe", MCTP_PHYS_BINDING_UCIE }, +}; + +static int parse_config_phys_binding(const char *type, + enum mctp_phys_binding *binding) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(phys_bindings); i++) { + if (!strcasecmp(type, phys_bindings[i].name)) { + *binding = phys_bindings[i].binding; + return 0; + } + } + + return -1; +} + static int fill_uuid(struct ctx *ctx) { int rc; @@ -5423,10 +5457,44 @@ enum match_result { MATCH_RES_ERR, }; +static enum match_result +parse_config_interface_match_phys_binding(toml_table_t *table, + struct interface_config_match *match) +{ + static const char *key = "phys-type"; + enum mctp_phys_binding binding; + toml_datum_t val; + int rc; + + if (!toml_key_exists(table, key)) + return MATCH_RES_NONE; + + val = toml_string_in(table, key); + if (!val.ok) { + warnx("invalid %s match", key); + return MATCH_RES_ERR; + } + + rc = parse_config_phys_binding(val.u.s, &binding); + if (rc) { + warnx("invalid %s value %s", key, val.u.s); + free(val.u.s); + return MATCH_RES_ERR; + } + free(val.u.s); + + match->type = IFACE_MATCH_BINDING; + match->binding = binding; + + return MATCH_RES_OK; +} + const struct match_parser { enum match_result (*parse)(toml_table_t *, struct interface_config_match *); -} match_parsers[] = {}; +} match_parsers[] = { + { parse_config_interface_match_phys_binding }, +}; static int parse_config_interface_match(struct ctx *ctx, unsigned int idx, toml_table_t *interface, @@ -5460,9 +5528,6 @@ static int parse_config_interface_match(struct ctx *ctx, unsigned int idx, return -1; } -// while match_parsers[] is empty -#pragma GCC diagnostic ignored "-Wtype-limits" - for (i = 0; i < ARRAY_SIZE(match_parsers); i++) { const struct match_parser *p = &match_parsers[i]; enum match_result mr; diff --git a/tests/test_mctpd.py b/tests/test_mctpd.py index 461fbaf5..3ca7c351 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -15,6 +15,7 @@ MCTPSockAddr, MCTPControlCommand, MctpdWrapper, + PhysicalBinding, VDMType, ) @@ -2014,3 +2015,26 @@ async def test_iface_config_match_all(dbus, sysnet, nursery): assert role == "BusOwner" res = await mctpd.stop_mctpd() assert res == 0 + + +async def test_iface_config_match_phys_binding(dbus, sysnet, nursery): + """Test that we can match an interface from a phys binding type""" + config = """ + role = "unknown" + [[interface]] + match = { phys-type = "i2c" } + role = "bus-owner" + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + iface = mctpd.system.interfaces[0] + iface.phys_binding = PhysicalBinding.SMBUS + + await mctpd.start_mctpd(nursery) + + iface = await mctpd_mctp_iface_control_obj(dbus, iface) + role = await iface.get_role() + assert role == "BusOwner" + + res = await mctpd.stop_mctpd() + assert res == 0 From 94c7317c4ca786ecc139d834df3060eb9629a0a2 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Tue, 24 Feb 2026 14:15:25 +0800 Subject: [PATCH 09/11] mctpd: resolve sysfs paths for links Look up the sysfs path for a MCTP interface, and store it on the link. We use the ops facility for this, to allow the test infrastructure to set arbitrary sysfs paths for simulated links. Signed-off-by: Jeremy Kerr --- src/mctp-ops.c | 34 ++++++++++++++++++++++++++++++++ src/mctp-ops.h | 1 + src/mctpd.c | 8 ++++++++ tests/mctp-ops-test.c | 41 +++++++++++++++++++++++++++++++++++++++ tests/mctpenv/__init__.py | 19 ++++++++++++++++++ 5 files changed, 103 insertions(+) diff --git a/src/mctp-ops.c b/src/mctp-ops.c index 7e681012..f2d970aa 100644 --- a/src/mctp-ops.c +++ b/src/mctp-ops.c @@ -7,6 +7,9 @@ #define _GNU_SOURCE +#include +#include +#include #include #include #include @@ -52,6 +55,36 @@ static int mctp_op_close(int sd) return close(sd); } +static int mctp_op_link_sysfs_path(const char *ifname, char **path) +{ + char *dev_class_path = NULL, *dev_path = NULL; + int rc = 1; + + rc = asprintf(&dev_class_path, "/sys/class/net/%s/device", ifname); + if (rc < 0) + return -1; + + dev_path = realpath(dev_class_path, NULL); + if (!dev_path) { + warnx("no path data for interface %s", ifname); + goto out; + } + + if (!strncmp(dev_path, "/sys", strlen("/sys"))) { + warnx("malformed interface path for %s", ifname); + goto out; + } + + *path = strdup(dev_path + 4); + rc = 0; + +out: + free(dev_path); + free(dev_class_path); + return rc; + return -1; +} + static void mctp_bug_warn(const char *fmt, va_list args) { vwarnx(fmt, args); @@ -81,6 +114,7 @@ const struct mctp_ops mctp_ops = { }, #endif .bug_warn = mctp_bug_warn, + .link_sysfs_path = mctp_op_link_sysfs_path, }; void mctp_ops_init(void) diff --git a/src/mctp-ops.h b/src/mctp-ops.h index 39f9501e..161bd680 100644 --- a/src/mctp-ops.h +++ b/src/mctp-ops.h @@ -41,6 +41,7 @@ struct mctp_ops { struct sd_event_ops sd_event; #endif void (*bug_warn)(const char *fmt, va_list args); + int (*link_sysfs_path)(const char *ifname, char **devpath); }; extern const struct mctp_ops mctp_ops; diff --git a/src/mctpd.c b/src/mctpd.c index 99c6725b..bc0a1ad0 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -141,6 +141,7 @@ struct link { int ifindex; enum endpoint_role role; uint8_t phys_binding; + char *sysfs_path; char *path; sd_bus_slot *slot_iface; @@ -4734,6 +4735,7 @@ static void free_link(struct link *link) sd_bus_slot_unref(link->slot_iface); sd_bus_slot_unref(link->slot_busowner); free(link->path); + free(link->sysfs_path); free(link); } @@ -5122,6 +5124,11 @@ static int link_apply_configuration(struct ctx *ctx, struct link *link) return 0; } +static int link_resolve_sysfs_path(struct link *link, const char *ifname) +{ + return mctp_ops.link_sysfs_path(ifname, &link->sysfs_path); +} + static int add_interface(struct ctx *ctx, int ifindex) { int rc; @@ -5149,6 +5156,7 @@ 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_resolve_sysfs_path(link, ifname); rc = asprintf(&link->path, "%s/%s", MCTP_DBUS_PATH_LINKS, ifname); if (rc < 0) { rc = -ENOMEM; diff --git a/tests/mctp-ops-test.c b/tests/mctp-ops-test.c index c58ff91d..80cff5a2 100644 --- a/tests/mctp-ops-test.c +++ b/tests/mctp-ops-test.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -333,6 +334,45 @@ static int mctp_op_sd_event_source_set_time_relative(sd_event_source *s, } #endif +static int mctp_op_link_sysfs_path(const char *ifname, char **path) +{ + struct { + uint8_t opcode; + char ifname[IFNAMSIZ]; + } req; + struct { + uint8_t len; + char path[256]; + } resp; + size_t len; + ssize_t rc; + + len = strlen(ifname); + if (len > sizeof(req.ifname)) + errx(EXIT_FAILURE, "invalid interface name"); + + req.opcode = 0x04; + memcpy(req.ifname, ifname, len); + rc = send(control_sd, &req, len + sizeof(req.opcode), 0); + if (rc < 0) + err(EXIT_FAILURE, "control send error"); + + rc = recv(control_sd, &resp, sizeof(resp), 0); + if (rc <= 0) + err(EXIT_FAILURE, "control receive error"); + + if (sizeof(resp.len) + resp.len != (size_t)rc) + err(EXIT_FAILURE, "control receive parse error"); + + if (!resp.len) + return -1; + + resp.path[resp.len] = '\0'; + + *path = strndup(resp.path, resp.len); + return 0; +} + const struct mctp_ops mctp_ops = { .mctp = { .socket = mctp_op_mctp_socket, @@ -357,6 +397,7 @@ const struct mctp_ops mctp_ops = { }, #endif .bug_warn = mctp_bug_warn, + .link_sysfs_path = mctp_op_link_sysfs_path, }; void mctp_ops_init(void) diff --git a/tests/mctpenv/__init__.py b/tests/mctpenv/__init__.py index f2deb245..7478819b 100644 --- a/tests/mctpenv/__init__.py +++ b/tests/mctpenv/__init__.py @@ -81,6 +81,7 @@ def __init__( self.mtu = max_mtu self.up = up self.phys_binding = phys_binding + self.sysfs_path = '/devices/virtual/' + name def __str__(self): lladdrstr = ':'.join('%02x' % b for b in self.lladdr) @@ -297,6 +298,12 @@ def find_endpoint(self, addr): return iface, lladdr + def lookup_link_path(self, ifname: str): + iface = self.find_interface_by_name(ifname) + if iface is None: + return None + return iface.sysfs_path + def dump(self): print("system:") if self.interfaces: @@ -1345,6 +1352,18 @@ async def handle_control(self, nursery): await send_fd(self.sock_local, remote.fileno()) remote.close() nursery.start_soon(sd.run) + + elif op == 0x04: + # Link sysfs lookup + ifname = data[1:].decode('utf-8') + path = self.system.lookup_link_path(ifname) + if path is None: + data = b'\0' + else: + b = path.encode('utf-8') + data = bytes([len(b)]) + b + await self.sock_local.send(data) + else: print(f"unknown op {op}") From 0d795d698cc5b5796a79d1d99f55d5d41df54f3a Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Tue, 24 Feb 2026 16:08:30 +0800 Subject: [PATCH 10/11] mctpd: Allow interface configuration matches on sysfs path Now that we have sysfs path data for links, allow configuration matches on the path (including via globs). This provides a harware-topology-consistent method of specifying individual links. Signed-off-by: Jeremy Kerr --- docs/mctpd.md | 18 ++++++++++ src/mctpd.c | 36 ++++++++++++++++++++ tests/test_mctpd.py | 82 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) diff --git a/docs/mctpd.md b/docs/mctpd.md index a3891efc..aacc1f7a 100644 --- a/docs/mctpd.md +++ b/docs/mctpd.md @@ -446,3 +446,21 @@ match = { phys-type = "i2c" } Available binding types are: `SMBus` / `I2C`, `PCIe`, `USB`, `KCS`, `serial`, `I3C`, `MMBI`, or `UCIE`. Matches are case-insensitive. + +Match on a sysfs device path: + +```toml +# match on sysfs path +[[interface]] +match = { path = "/devices/pci0000:00/0000:00:08.3/usb10/10-0:1.0" } +``` + +Paths may use glob expressions: + +```toml +# match on globbed sysfs path +[[interface]] +match = { path = "/devices/pci0000:00/0000:00:08.3/*" } +``` + +Paths have the `/sys` prefix stripped. diff --git a/src/mctpd.c b/src/mctpd.c index bc0a1ad0..431167f7 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -250,9 +251,11 @@ struct interface_config { enum { IFACE_MATCH_ALL, IFACE_MATCH_BINDING, + IFACE_MATCH_PATH, } type; union { enum mctp_phys_binding binding; + char *path; }; } match; @@ -5092,6 +5095,10 @@ static bool config_link_match(struct interface_config_match *match, return true; case IFACE_MATCH_BINDING: return link->phys_binding == match->binding; + case IFACE_MATCH_PATH: + if (!link->sysfs_path) + return false; + return fnmatch(match->path, link->sysfs_path, 0) == 0; } return false; } @@ -5497,11 +5504,33 @@ parse_config_interface_match_phys_binding(toml_table_t *table, return MATCH_RES_OK; } +static enum match_result +parse_config_interface_match_path(toml_table_t *table, + struct interface_config_match *match) +{ + static const char *key = "path"; + toml_datum_t val; + + if (!toml_key_exists(table, key)) + return MATCH_RES_NONE; + + val = toml_string_in(table, key); + if (!val.ok) { + warnx("invalid path match"); + return MATCH_RES_ERR; + } + + match->type = IFACE_MATCH_PATH; + match->path = val.u.s; + return MATCH_RES_OK; +} + const struct match_parser { enum match_result (*parse)(toml_table_t *, struct interface_config_match *); } match_parsers[] = { { parse_config_interface_match_phys_binding }, + { parse_config_interface_match_path }, }; static int parse_config_interface_match(struct ctx *ctx, unsigned int idx, @@ -5748,7 +5777,14 @@ static void setup_config_defaults(struct ctx *ctx) static void free_config(struct ctx *ctx) { + unsigned int i; + free(ctx->config_filename); + for (i = 0; i < ctx->num_interface_configs; i++) { + struct interface_config *config = &ctx->interface_configs[i]; + if (config->match.type == IFACE_MATCH_PATH) + free(config->match.path); + } free(ctx->interface_configs); } diff --git a/tests/test_mctpd.py b/tests/test_mctpd.py index 3ca7c351..4d9dfd3d 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -2038,3 +2038,85 @@ async def test_iface_config_match_phys_binding(dbus, sysnet, nursery): res = await mctpd.stop_mctpd() assert res == 0 + + +async def test_iface_config_match_path_exact(dbus, sysnet, nursery): + """Test that we can match an interface from an exact path""" + config = """ + role = "unknown" + [[interface]] + match = { path = "/devices/virtual/mctp0" } + role = "bus-owner" + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + await mctpd.start_mctpd(nursery) + + iface = await mctpd_mctp_iface_control_obj(dbus, mctpd.system.interfaces[0]) + role = await iface.get_role() + assert role == "BusOwner" + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_iface_config_nomatch_path(dbus, sysnet, nursery): + """Test that we do not match an interface from an exact (non-matching) + path + """ + config = """ + role = "unknown" + [[interface]] + match = { path = "/devices/virtual/mctp1" } + role = "bus-owner" + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + await mctpd.start_mctpd(nursery) + + iface = await mctpd_mctp_iface_control_obj(dbus, mctpd.system.interfaces[0]) + role = await iface.get_role() + assert role == "Unknown" + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_iface_config_match_path_glob(dbus, sysnet, nursery): + """Test that we can match an interface from a globbed path""" + config = """ + role = "unknown" + [[interface]] + match = { path = "/devices/virtual/mctp*" } + role = "bus-owner" + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + await mctpd.start_mctpd(nursery) + + iface = await mctpd_mctp_iface_control_obj(dbus, mctpd.system.interfaces[0]) + role = await iface.get_role() + assert role == "BusOwner" + + res = await mctpd.stop_mctpd() + assert res == 0 + + +async def test_iface_config_match_path_none(dbus, sysnet, nursery): + """Test that we can handle a missing sysfs path, not matching anything""" + config = """ + role = "unknown" + [[interface]] + match = { path = "*" } + role = "bus-owner" + """ + + mctpd = MctpdWrapper(dbus, sysnet, config=config) + mctpd.system.interfaces[0].sysfs_path = None + await mctpd.start_mctpd(nursery) + + iface = await mctpd_mctp_iface_control_obj(dbus, mctpd.system.interfaces[0]) + role = await iface.get_role() + assert role != "BusOwner" + + res = await mctpd.stop_mctpd() + assert res == 0 From 81844bb4b886de80fbf7d76c95b3cad00242d626 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Tue, 24 Feb 2026 17:00:08 +0800 Subject: [PATCH 11/11] CHANGELOG: Add entry for link configuration facility Signed-off-by: Jeremy Kerr --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a8ab043..29483b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 2. `mctp`'s verbose mode will now decode `IFLA_MCTP` attributes in netlink message data +3. `mctpd` now supports configuration on individual links, without having + to perform dbus property updates. Links may be matched on physical transport + binding type, or by sysfs paths, allowing individual interface roles to be + specified by the configuration file. + ### Fixes 1. mctpd's interface objects now expose the BusOwner1 interface when set