From 80bda24384094414af18c45d945859e8592101f0 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 1f28346..025d05a 100644 --- a/src/mctpd.c +++ b/src/mctpd.c @@ -5207,7 +5207,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; @@ -5217,7 +5217,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; } @@ -5391,7 +5391,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 6eb010f94670ad8530590dc26111201507bec448 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 b950d05..d58538e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 1. mctpd's interface objects now expose the BusOwner1 interface when set as a BusOwner via the Role property +### 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 d505d11..b6f95f1 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 4e866e3..a7b0ef3 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 025d05a..cba31c4 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; } @@ -5090,7 +5090,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) { @@ -5207,21 +5207,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; } @@ -5389,9 +5389,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 0785cae..f29a967 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 23f210c0fcfe3f69ad78cf161fd6ef8917a4a394 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 a7b0ef3..244dfeb 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 f7e4c51b3dd7e32b06cedc815594e9e46e84c012 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 d1de8d0..fa8b9a2 100644 --- a/tests/mctpenv/__init__.py +++ b/tests/mctpenv/__init__.py @@ -1294,9 +1294,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 @@ -1340,18 +1341,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, ) @@ -1430,11 +1431,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 d26599c7e5a17347c94fc22f495cd01aab04cfde 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 cba31c4..a7de036 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; @@ -5080,8 +5081,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; @@ -5090,6 +5089,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); @@ -5115,7 +5115,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 16ff61ca5aa42d4457ee3b0b12e01032a362a77a 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 244dfeb..8e55384 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 a7de036..dbdec13 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); @@ -5065,6 +5080,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; @@ -5098,6 +5150,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); @@ -5357,9 +5416,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; @@ -5414,6 +5597,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: @@ -5467,6 +5657,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 955ce38..2b58610 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, @@ -1956,3 +1957,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 640b1d885fb1c9031c956dc601611d9220e1d5ee 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 8e55384..5a754d1 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 dbdec13..7772cb3 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 { @@ -5112,7 +5115,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; } @@ -5487,6 +5491,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); @@ -5495,6 +5500,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 2b58610..9d1b3d2 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -1972,3 +1972,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 60e3bfb1b67b45eb09493957c4c4d1c6d4291a99 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 | 31 ++++++++++++++++++- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/docs/mctpd.md b/docs/mctpd.md index 5a754d1..a3891ef 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 7772cb3..2c403cb 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; @@ -5089,6 +5091,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; } @@ -5288,6 +5292,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; @@ -5426,10 +5460,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, @@ -5463,9 +5531,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 9d1b3d2..ddd87d1 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -10,7 +10,13 @@ mctpd_mctp_endpoint_control_obj, mctpd_mctp_base_iface_obj, ) -from mctpenv import Endpoint, MCTPSockAddr, MCTPControlCommand, MctpdWrapper +from mctpenv import ( + Endpoint, + MCTPSockAddr, + MCTPControlCommand, + MctpdWrapper, + PhysicalBinding, +) # DBus constant symbol suffixes: # @@ -1990,3 +1996,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 bbf85a7f6f219c52106f2de3131156ee5b029caa 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 7e68101..f2d970a 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 39f9501..161bd68 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 2c403cb..48591f7 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; @@ -4737,6 +4738,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); } @@ -5125,6 +5127,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; @@ -5152,6 +5159,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 c58ff91..80cff5a 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 fa8b9a2..2219962 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: @@ -1289,6 +1296,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 5517c911dfbba8ec41901e47f99202ce701c744f 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 a3891ef..aacc1f7 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 48591f7..28d842c 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; @@ -5095,6 +5098,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; } @@ -5500,11 +5507,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, @@ -5751,7 +5780,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 ddd87d1..06d65bd 100644 --- a/tests/test_mctpd.py +++ b/tests/test_mctpd.py @@ -2019,3 +2019,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 93c68f5973bb29f9848ecc8250d20c2d5a88bfa5 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 d58538e..2d7e82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 1. `mctpd` now queries endpoints for their vendor-defined message support, and publishes as the newly-specced `VendorDefinedMessageTypes` dbus property. +2. `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