From: Abdul Wasey <w453y.me@gmail.com>
To: netdev@vger.kernel.org
Cc: dsahern@kernel.org, stephen@networkplumber.org,
idosch@nvidia.com, razor@blackwall.org,
horatiu.vultur@microchip.com, bridge@lists.linux.dev
Subject: [PATCH iproute2-next 2/2] bridge: add cfm command
Date: Sun, 4 Oct 2026 04:13:07 +0000 [thread overview]
Message-ID: <20261004041307.2683990-3-w453y.me@gmail.com> (raw)
In-Reply-To: <20261004041307.2683990-1-w453y.me@gmail.com>
The bridge has had 802.1ag CFM support since 5.11 (net/bridge/br_cfm.c),
but iproute2 never got a command for it, so the only way to use it is
Microchip's out of tree cfm tool.
Add "bridge cfm", which covers what the kernel supports:
bridge cfm mep { add | del | set }
bridge cfm cc config
bridge cfm cc peer { add | del }
bridge cfm cc rdi
bridge cfm cc ccm-tx
bridge cfm show { config | status }
Each command maps to one IFLA_BRIDGE_CFM_* group and is sent to the
bridge device with BRIDGE_FLAGS_SELF. The MAID is taken as a hex string,
the same bytes the kernel stores. There is no way yet to build it from
an MD and MA name.
Example with a MEP on each end of a veth pair, one bridge each side:
$ bridge cfm mep add bridge brA instance 1 port vA
$ bridge cfm mep set bridge brA instance 1 \
address 02:00:00:00:00:0a level 3 mepid 10
$ bridge cfm cc config bridge brA instance 1 \
enable on interval 100ms maid 0104746573740002
$ bridge cfm cc peer add bridge brA instance 1 mepid 20
$ bridge cfm cc ccm-tx bridge brA instance 1 \
dmac 01:80:c2:00:00:33 period 30
(same on brB with mepid 20 and peer 10)
$ bridge cfm show status bridge brA
brA
mep 1 opcode_unexp_seen false version_unexp_seen false ...
peer mepid 20 ccm_defect false rdi false ... seen true ...
Signed-off-by: Abdul Wasey <w453y.me@gmail.com>
---
bridge/Makefile | 2 +-
bridge/br_common.h | 1 +
bridge/bridge.c | 3 +-
bridge/cfm.c | 1058 ++++++++++++++++++++++++++++++++++++++++++++
man/man8/bridge.8 | 196 +++++++-
5 files changed, 1257 insertions(+), 3 deletions(-)
create mode 100644 bridge/cfm.c
diff --git a/bridge/Makefile b/bridge/Makefile
index 4c57df43..4bf5b529 100644
--- a/bridge/Makefile
+++ b/bridge/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
-BROBJ = bridge.o fdb.o monitor.o link.o mdb.o mst.o vlan.o vni.o
+BROBJ = bridge.o fdb.o monitor.o link.o mdb.o mst.o vlan.o vni.o cfm.o
include ../config.mk
diff --git a/bridge/br_common.h b/bridge/br_common.h
index 3a0cf882..fb9b0d5a 100644
--- a/bridge/br_common.h
+++ b/bridge/br_common.h
@@ -24,6 +24,7 @@ int do_mst(int argc, char **argv);
int do_vlan(int argc, char **argv);
int do_link(int argc, char **argv);
int do_vni(int argc, char **argv);
+int do_cfm(int argc, char **argv);
extern int preferred_family;
extern int show_stats;
diff --git a/bridge/bridge.c b/bridge/bridge.c
index d993ba19..1c3bde32 100644
--- a/bridge/bridge.c
+++ b/bridge/bridge.c
@@ -36,7 +36,7 @@ static void usage(void)
fprintf(stderr,
"Usage: bridge [ OPTIONS ] OBJECT { COMMAND | help }\n"
" bridge [ -force ] -batch filename\n"
-"where OBJECT := { link | fdb | mdb | mst | vlan | vni | monitor }\n"
+"where OBJECT := { link | fdb | mdb | mst | vlan | vni | cfm | monitor }\n"
" OPTIONS := { -V[ersion] | -s[tatistics] | -d[etails] |\n"
" -o[neline] | -t[imestamp] | -n[etns] name |\n"
" -com[pressvlans] -c[olor] -p[retty] -j[son] }\n");
@@ -59,6 +59,7 @@ static const struct cmd {
{ "mst", do_mst },
{ "vlan", do_vlan },
{ "vni", do_vni },
+ { "cfm", do_cfm },
{ "monitor", do_monitor },
{ "help", do_help },
{ 0 }
diff --git a/bridge/cfm.c b/bridge/cfm.c
new file mode 100644
index 00000000..1b7fb159
--- /dev/null
+++ b/bridge/cfm.c
@@ -0,0 +1,1058 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * bridge cfm: configure and show IEEE 802.1ag Connectivity Fault Management
+ * (CFM) Maintenance Endpoints (MEPs) on a Linux bridge.
+ *
+ * Authors: Abdul Wasey <w453y.me@gmail.com>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <net/if.h>
+#include <linux/if_bridge.h>
+#include <linux/cfm_bridge.h>
+#include <linux/rtnetlink.h>
+
+#include "libnetlink.h"
+#include "utils.h"
+#include "br_common.h"
+#include "json_print.h"
+#include "rt_names.h"
+
+static unsigned int filter_index;
+
+static void cfm_usage(void)
+{
+ fprintf(stderr,
+"Usage: bridge cfm mep add bridge DEV instance INSTANCE port PORT\n"
+" [ domain port ] [ direction down ]\n"
+" bridge cfm mep del bridge DEV instance INSTANCE\n"
+" bridge cfm mep set bridge DEV instance INSTANCE address LLADDR\n"
+" level LEVEL mepid MEPID\n"
+" bridge cfm cc config bridge DEV instance INSTANCE enable { on | off }\n"
+" interval INTERVAL maid MAID\n"
+" bridge cfm cc peer { add | del } bridge DEV instance INSTANCE mepid MEPID\n"
+" bridge cfm cc rdi bridge DEV instance INSTANCE rdi { on | off }\n"
+" bridge cfm cc ccm-tx bridge DEV instance INSTANCE dmac LLADDR period PERIOD\n"
+" [ seq { on | off } ]\n"
+" [ iftlv { on | off } ] [ iftlv-value VALUE ]\n"
+" [ porttlv { on | off } ] [ porttlv-value VALUE ]\n"
+" bridge cfm show { config | status } [ bridge DEV ]\n"
+"\n"
+"where INTERVAL := { 3ms3 | 10ms | 100ms | 1s | 10s | 1m | 10m }\n"
+" MAID is the 48 byte MAID as a hex string, zero padded if shorter\n"
+" PERIOD is how many seconds to send CCMs for, 0 stops sending\n"
+" seq on puts an incrementing sequence number in each CCM\n");
+ exit(-1);
+}
+
+static int cfm_parse_interval(const char *str)
+{
+ if (!strcmp(str, "none"))
+ return BR_CFM_CCM_INTERVAL_NONE;
+ if (!strcmp(str, "3ms3") || !strcmp(str, "3.3ms"))
+ return BR_CFM_CCM_INTERVAL_3_3_MS;
+ if (!strcmp(str, "10ms"))
+ return BR_CFM_CCM_INTERVAL_10_MS;
+ if (!strcmp(str, "100ms"))
+ return BR_CFM_CCM_INTERVAL_100_MS;
+ if (!strcmp(str, "1s"))
+ return BR_CFM_CCM_INTERVAL_1_SEC;
+ if (!strcmp(str, "10s"))
+ return BR_CFM_CCM_INTERVAL_10_SEC;
+ if (!strcmp(str, "1m"))
+ return BR_CFM_CCM_INTERVAL_1_MIN;
+ if (!strcmp(str, "10m"))
+ return BR_CFM_CCM_INTERVAL_10_MIN;
+ return -1;
+}
+
+static const char *cfm_interval_str(__u32 interval)
+{
+ switch (interval) {
+ case BR_CFM_CCM_INTERVAL_NONE:
+ return "none";
+ case BR_CFM_CCM_INTERVAL_3_3_MS:
+ return "3ms3";
+ case BR_CFM_CCM_INTERVAL_10_MS:
+ return "10ms";
+ case BR_CFM_CCM_INTERVAL_100_MS:
+ return "100ms";
+ case BR_CFM_CCM_INTERVAL_1_SEC:
+ return "1s";
+ case BR_CFM_CCM_INTERVAL_10_SEC:
+ return "10s";
+ case BR_CFM_CCM_INTERVAL_1_MIN:
+ return "1m";
+ case BR_CFM_CCM_INTERVAL_10_MIN:
+ return "10m";
+ default:
+ return "unknown";
+ }
+}
+
+static int cfm_talk(struct nlmsghdr *n)
+{
+ return rtnl_talk(&rth, n, NULL);
+}
+
+/* "bridge cfm mep add" / "bridge cfm mep del" / "bridge cfm mep set" */
+
+static int cfm_mep_add(int argc, char **argv)
+{
+ struct {
+ struct nlmsghdr n;
+ struct ifinfomsg ifm;
+ char buf[1024];
+ } req = {
+ .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
+ .n.nlmsg_flags = NLM_F_REQUEST,
+ .n.nlmsg_type = RTM_SETLINK,
+ .ifm.ifi_family = PF_BRIDGE,
+ };
+ struct rtattr *afspec, *cfm, *create;
+ __u32 instance = 0, domain = BR_CFM_PORT, direction = BR_CFM_MEP_DIRECTION_DOWN;
+ __u32 port_idx = 0;
+ char *bridge_dev = NULL, *port_dev = NULL;
+ bool instance_set = false;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "bridge") == 0) {
+ NEXT_ARG();
+ bridge_dev = *argv;
+ } else if (strcmp(*argv, "instance") == 0) {
+ NEXT_ARG();
+ if (get_u32(&instance, *argv, 0))
+ invarg("invalid instance", *argv);
+ instance_set = true;
+ } else if (strcmp(*argv, "domain") == 0) {
+ NEXT_ARG();
+ if (strcmp(*argv, "port") == 0)
+ domain = BR_CFM_PORT;
+ else if (strcmp(*argv, "vlan") == 0)
+ domain = BR_CFM_VLAN;
+ else
+ invarg("invalid domain", *argv);
+ } else if (strcmp(*argv, "direction") == 0) {
+ NEXT_ARG();
+ if (strcmp(*argv, "down") == 0)
+ direction = BR_CFM_MEP_DIRECTION_DOWN;
+ else if (strcmp(*argv, "up") == 0)
+ direction = BR_CFM_MEP_DIRECTION_UP;
+ else
+ invarg("invalid direction", *argv);
+ } else if (strcmp(*argv, "port") == 0) {
+ NEXT_ARG();
+ port_dev = *argv;
+ } else {
+ cfm_usage();
+ }
+ argc--; argv++;
+ }
+
+ if (!bridge_dev || !instance_set || !port_dev) {
+ fprintf(stderr,
+ "Required: bridge DEV instance INSTANCE port PORT_DEV\n");
+ return -1;
+ }
+
+ req.ifm.ifi_index = ll_name_to_index(bridge_dev);
+ if (!req.ifm.ifi_index)
+ return nodev(bridge_dev);
+
+ port_idx = ll_name_to_index(port_dev);
+ if (!port_idx)
+ return nodev(port_dev);
+
+ afspec = addattr_nest(&req.n, sizeof(req), IFLA_AF_SPEC);
+ addattr16(&req.n, sizeof(req), IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF);
+ cfm = addattr_nest(&req.n, sizeof(req), IFLA_BRIDGE_CFM | NLA_F_NESTED);
+ create = addattr_nest(&req.n, sizeof(req),
+ IFLA_BRIDGE_CFM_MEP_CREATE | NLA_F_NESTED);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_CREATE_INSTANCE,
+ instance);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_CREATE_DOMAIN,
+ domain);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_CREATE_DIRECTION,
+ direction);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_CREATE_IFINDEX,
+ port_idx);
+ addattr_nest_end(&req.n, create);
+ addattr_nest_end(&req.n, cfm);
+ addattr_nest_end(&req.n, afspec);
+
+ return cfm_talk(&req.n);
+}
+
+static int cfm_mep_del(int argc, char **argv)
+{
+ struct {
+ struct nlmsghdr n;
+ struct ifinfomsg ifm;
+ char buf[1024];
+ } req = {
+ .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
+ .n.nlmsg_flags = NLM_F_REQUEST,
+ .n.nlmsg_type = RTM_SETLINK,
+ .ifm.ifi_family = PF_BRIDGE,
+ };
+ struct rtattr *afspec, *cfm, *del;
+ __u32 instance = 0;
+ char *bridge_dev = NULL;
+ bool instance_set = false;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "bridge") == 0) {
+ NEXT_ARG();
+ bridge_dev = *argv;
+ } else if (strcmp(*argv, "instance") == 0) {
+ NEXT_ARG();
+ if (get_u32(&instance, *argv, 0))
+ invarg("invalid instance", *argv);
+ instance_set = true;
+ } else {
+ cfm_usage();
+ }
+ argc--; argv++;
+ }
+
+ if (!bridge_dev || !instance_set) {
+ fprintf(stderr, "Required: bridge DEV instance INSTANCE\n");
+ return -1;
+ }
+
+ req.ifm.ifi_index = ll_name_to_index(bridge_dev);
+ if (!req.ifm.ifi_index)
+ return nodev(bridge_dev);
+
+ afspec = addattr_nest(&req.n, sizeof(req), IFLA_AF_SPEC);
+ addattr16(&req.n, sizeof(req), IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF);
+ cfm = addattr_nest(&req.n, sizeof(req), IFLA_BRIDGE_CFM | NLA_F_NESTED);
+ del = addattr_nest(&req.n, sizeof(req),
+ IFLA_BRIDGE_CFM_MEP_DELETE | NLA_F_NESTED);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_DELETE_INSTANCE,
+ instance);
+ addattr_nest_end(&req.n, del);
+ addattr_nest_end(&req.n, cfm);
+ addattr_nest_end(&req.n, afspec);
+
+ return cfm_talk(&req.n);
+}
+
+static int cfm_mep_set(int argc, char **argv)
+{
+ struct {
+ struct nlmsghdr n;
+ struct ifinfomsg ifm;
+ char buf[1024];
+ } req = {
+ .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
+ .n.nlmsg_flags = NLM_F_REQUEST,
+ .n.nlmsg_type = RTM_SETLINK,
+ .ifm.ifi_family = PF_BRIDGE,
+ };
+ struct rtattr *afspec, *cfm, *config;
+ __u32 instance = 0, level = 0, mepid = 0;
+ char *bridge_dev = NULL, *address = NULL;
+ bool instance_set = false, level_set = false, mepid_set = false;
+ char abuf[32];
+ int alen = 0;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "bridge") == 0) {
+ NEXT_ARG();
+ bridge_dev = *argv;
+ } else if (strcmp(*argv, "instance") == 0) {
+ NEXT_ARG();
+ if (get_u32(&instance, *argv, 0))
+ invarg("invalid instance", *argv);
+ instance_set = true;
+ } else if (strcmp(*argv, "address") == 0) {
+ NEXT_ARG();
+ address = *argv;
+ } else if (strcmp(*argv, "level") == 0) {
+ NEXT_ARG();
+ if (get_u32(&level, *argv, 0) || level > 7)
+ invarg("invalid level (0-7)", *argv);
+ level_set = true;
+ } else if (strcmp(*argv, "mepid") == 0) {
+ NEXT_ARG();
+ if (get_u32(&mepid, *argv, 0) || mepid > 0x1FFF)
+ invarg("invalid mepid (0-8191)", *argv);
+ mepid_set = true;
+ } else {
+ cfm_usage();
+ }
+ argc--; argv++;
+ }
+
+ if (!bridge_dev || !instance_set || !address ||
+ !level_set || !mepid_set) {
+ fprintf(stderr,
+ "Required: bridge DEV instance INSTANCE address LLADDR level LEVEL mepid MEPID\n");
+ return -1;
+ }
+
+ req.ifm.ifi_index = ll_name_to_index(bridge_dev);
+ if (!req.ifm.ifi_index)
+ return nodev(bridge_dev);
+
+ alen = ll_addr_a2n(abuf, sizeof(abuf), address);
+ if (alen != ETH_ALEN) {
+ fprintf(stderr, "Invalid address %s\n", address);
+ return -1;
+ }
+
+ afspec = addattr_nest(&req.n, sizeof(req), IFLA_AF_SPEC);
+ addattr16(&req.n, sizeof(req), IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF);
+ cfm = addattr_nest(&req.n, sizeof(req), IFLA_BRIDGE_CFM | NLA_F_NESTED);
+ config = addattr_nest(&req.n, sizeof(req),
+ IFLA_BRIDGE_CFM_MEP_CONFIG | NLA_F_NESTED);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_CONFIG_INSTANCE,
+ instance);
+ addattr_l(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_CONFIG_UNICAST_MAC,
+ abuf, ETH_ALEN);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_CONFIG_MDLEVEL,
+ level);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_MEP_CONFIG_MEPID,
+ mepid);
+ addattr_nest_end(&req.n, config);
+ addattr_nest_end(&req.n, cfm);
+ addattr_nest_end(&req.n, afspec);
+
+ return cfm_talk(&req.n);
+}
+
+static int cfm_mep(int argc, char **argv)
+{
+ if (argc <= 0)
+ cfm_usage();
+
+ if (strcmp(*argv, "add") == 0)
+ return cfm_mep_add(argc - 1, argv + 1);
+ if (strcmp(*argv, "delete") == 0 || strcmp(*argv, "del") == 0)
+ return cfm_mep_del(argc - 1, argv + 1);
+ if (strcmp(*argv, "set") == 0)
+ return cfm_mep_set(argc - 1, argv + 1);
+
+ cfm_usage();
+ return -1;
+}
+
+/* "bridge cfm cc ..." */
+
+static int cfm_cc_config(int argc, char **argv)
+{
+ struct {
+ struct nlmsghdr n;
+ struct ifinfomsg ifm;
+ char buf[1024];
+ } req = {
+ .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
+ .n.nlmsg_flags = NLM_F_REQUEST,
+ .n.nlmsg_type = RTM_SETLINK,
+ .ifm.ifi_family = PF_BRIDGE,
+ };
+ struct rtattr *afspec, *cfm, *config;
+ __u32 instance = 0;
+ int enable = -1, interval = -1;
+ char *bridge_dev = NULL, *maid_str = NULL;
+ bool instance_set = false;
+ unsigned char maid[CFM_MAID_LENGTH] = {};
+ unsigned int maid_len = 0;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "bridge") == 0) {
+ NEXT_ARG();
+ bridge_dev = *argv;
+ } else if (strcmp(*argv, "instance") == 0) {
+ NEXT_ARG();
+ if (get_u32(&instance, *argv, 0))
+ invarg("invalid instance", *argv);
+ instance_set = true;
+ } else if (strcmp(*argv, "enable") == 0) {
+ int ret;
+
+ NEXT_ARG();
+ enable = parse_on_off("enable", *argv, &ret);
+ if (ret)
+ return ret;
+ } else if (strcmp(*argv, "interval") == 0) {
+ NEXT_ARG();
+ interval = cfm_parse_interval(*argv);
+ if (interval < 0)
+ invarg("invalid interval", *argv);
+ } else if (strcmp(*argv, "maid") == 0) {
+ NEXT_ARG();
+ maid_str = *argv;
+ } else {
+ cfm_usage();
+ }
+ argc--; argv++;
+ }
+
+ if (!bridge_dev || !instance_set || enable < 0 || interval < 0 ||
+ !maid_str) {
+ fprintf(stderr,
+ "Required: bridge DEV instance INSTANCE enable { on | off } interval INTERVAL maid MAID\n");
+ return -1;
+ }
+
+ req.ifm.ifi_index = ll_name_to_index(bridge_dev);
+ if (!req.ifm.ifi_index)
+ return nodev(bridge_dev);
+
+ if (!hexstring_a2n(maid_str, maid, sizeof(maid), &maid_len)) {
+ fprintf(stderr, "Invalid maid %s\n", maid_str);
+ return -1;
+ }
+
+ afspec = addattr_nest(&req.n, sizeof(req), IFLA_AF_SPEC);
+ addattr16(&req.n, sizeof(req), IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF);
+ cfm = addattr_nest(&req.n, sizeof(req), IFLA_BRIDGE_CFM | NLA_F_NESTED);
+ config = addattr_nest(&req.n, sizeof(req),
+ IFLA_BRIDGE_CFM_CC_CONFIG | NLA_F_NESTED);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CONFIG_INSTANCE,
+ instance);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CONFIG_ENABLE,
+ enable);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CONFIG_EXP_INTERVAL,
+ interval);
+ addattr_l(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CONFIG_EXP_MAID, maid,
+ sizeof(maid));
+ addattr_nest_end(&req.n, config);
+ addattr_nest_end(&req.n, cfm);
+ addattr_nest_end(&req.n, afspec);
+
+ return cfm_talk(&req.n);
+}
+
+static int cfm_cc_peer(int argc, char **argv, bool add)
+{
+ struct {
+ struct nlmsghdr n;
+ struct ifinfomsg ifm;
+ char buf[1024];
+ } req = {
+ .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
+ .n.nlmsg_flags = NLM_F_REQUEST,
+ .n.nlmsg_type = RTM_SETLINK,
+ .ifm.ifi_family = PF_BRIDGE,
+ };
+ struct rtattr *afspec, *cfm, *peer;
+ __u32 instance = 0, mepid = 0;
+ char *bridge_dev = NULL;
+ bool instance_set = false, mepid_set = false;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "bridge") == 0) {
+ NEXT_ARG();
+ bridge_dev = *argv;
+ } else if (strcmp(*argv, "instance") == 0) {
+ NEXT_ARG();
+ if (get_u32(&instance, *argv, 0))
+ invarg("invalid instance", *argv);
+ instance_set = true;
+ } else if (strcmp(*argv, "mepid") == 0) {
+ NEXT_ARG();
+ if (get_u32(&mepid, *argv, 0) || mepid > 0x1FFF)
+ invarg("invalid mepid (0-8191)", *argv);
+ mepid_set = true;
+ } else {
+ cfm_usage();
+ }
+ argc--; argv++;
+ }
+
+ if (!bridge_dev || !instance_set || !mepid_set) {
+ fprintf(stderr,
+ "Required: bridge DEV instance INSTANCE mepid MEPID\n");
+ return -1;
+ }
+
+ req.ifm.ifi_index = ll_name_to_index(bridge_dev);
+ if (!req.ifm.ifi_index)
+ return nodev(bridge_dev);
+
+ afspec = addattr_nest(&req.n, sizeof(req), IFLA_AF_SPEC);
+ addattr16(&req.n, sizeof(req), IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF);
+ cfm = addattr_nest(&req.n, sizeof(req), IFLA_BRIDGE_CFM | NLA_F_NESTED);
+ peer = addattr_nest(&req.n, sizeof(req),
+ (add ? IFLA_BRIDGE_CFM_CC_PEER_MEP_ADD :
+ IFLA_BRIDGE_CFM_CC_PEER_MEP_REMOVE) |
+ NLA_F_NESTED);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_PEER_MEP_INSTANCE,
+ instance);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_PEER_MEPID, mepid);
+ addattr_nest_end(&req.n, peer);
+ addattr_nest_end(&req.n, cfm);
+ addattr_nest_end(&req.n, afspec);
+
+ return cfm_talk(&req.n);
+}
+
+static int cfm_cc_rdi(int argc, char **argv)
+{
+ struct {
+ struct nlmsghdr n;
+ struct ifinfomsg ifm;
+ char buf[1024];
+ } req = {
+ .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
+ .n.nlmsg_flags = NLM_F_REQUEST,
+ .n.nlmsg_type = RTM_SETLINK,
+ .ifm.ifi_family = PF_BRIDGE,
+ };
+ struct rtattr *afspec, *cfm, *rdi;
+ __u32 instance = 0;
+ int rdi_val = -1;
+ char *bridge_dev = NULL;
+ bool instance_set = false;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "bridge") == 0) {
+ NEXT_ARG();
+ bridge_dev = *argv;
+ } else if (strcmp(*argv, "instance") == 0) {
+ NEXT_ARG();
+ if (get_u32(&instance, *argv, 0))
+ invarg("invalid instance", *argv);
+ instance_set = true;
+ } else if (strcmp(*argv, "rdi") == 0) {
+ int ret;
+
+ NEXT_ARG();
+ rdi_val = parse_on_off("rdi", *argv, &ret);
+ if (ret)
+ return ret;
+ } else {
+ cfm_usage();
+ }
+ argc--; argv++;
+ }
+
+ if (!bridge_dev || !instance_set || rdi_val < 0) {
+ fprintf(stderr,
+ "Required: bridge DEV instance INSTANCE rdi { on | off }\n");
+ return -1;
+ }
+
+ req.ifm.ifi_index = ll_name_to_index(bridge_dev);
+ if (!req.ifm.ifi_index)
+ return nodev(bridge_dev);
+
+ afspec = addattr_nest(&req.n, sizeof(req), IFLA_AF_SPEC);
+ addattr16(&req.n, sizeof(req), IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF);
+ cfm = addattr_nest(&req.n, sizeof(req), IFLA_BRIDGE_CFM | NLA_F_NESTED);
+ rdi = addattr_nest(&req.n, sizeof(req),
+ IFLA_BRIDGE_CFM_CC_RDI | NLA_F_NESTED);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_RDI_INSTANCE,
+ instance);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_RDI_RDI, rdi_val);
+ addattr_nest_end(&req.n, rdi);
+ addattr_nest_end(&req.n, cfm);
+ addattr_nest_end(&req.n, afspec);
+
+ return cfm_talk(&req.n);
+}
+
+static int cfm_cc_ccm_tx(int argc, char **argv)
+{
+ struct {
+ struct nlmsghdr n;
+ struct ifinfomsg ifm;
+ char buf[1024];
+ } req = {
+ .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
+ .n.nlmsg_flags = NLM_F_REQUEST,
+ .n.nlmsg_type = RTM_SETLINK,
+ .ifm.ifi_family = PF_BRIDGE,
+ };
+ struct rtattr *afspec, *cfm, *tx;
+ __u32 instance = 0, period = 0;
+ __u32 iftlv = 0, porttlv = 0;
+ __u8 iftlv_value = 0, porttlv_value = 0;
+ int seq = 0;
+ char *bridge_dev = NULL, *dmac = NULL;
+ bool instance_set = false, period_set = false;
+ char abuf[32];
+ int alen;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "bridge") == 0) {
+ NEXT_ARG();
+ bridge_dev = *argv;
+ } else if (strcmp(*argv, "instance") == 0) {
+ NEXT_ARG();
+ if (get_u32(&instance, *argv, 0))
+ invarg("invalid instance", *argv);
+ instance_set = true;
+ } else if (strcmp(*argv, "dmac") == 0) {
+ NEXT_ARG();
+ dmac = *argv;
+ } else if (strcmp(*argv, "seq") == 0) {
+ int ret;
+
+ NEXT_ARG();
+ seq = parse_on_off("seq", *argv, &ret);
+ if (ret)
+ return ret;
+ } else if (strcmp(*argv, "period") == 0) {
+ NEXT_ARG();
+ if (get_u32(&period, *argv, 0))
+ invarg("invalid period", *argv);
+ period_set = true;
+ } else if (strcmp(*argv, "iftlv") == 0) {
+ int ret;
+
+ NEXT_ARG();
+ iftlv = parse_on_off("iftlv", *argv, &ret);
+ if (ret)
+ return ret;
+ } else if (strcmp(*argv, "iftlv-value") == 0) {
+ __u32 v;
+
+ NEXT_ARG();
+ if (get_u32(&v, *argv, 0) || v > 0xff)
+ invarg("invalid iftlv-value", *argv);
+ iftlv_value = v;
+ } else if (strcmp(*argv, "porttlv") == 0) {
+ int ret;
+
+ NEXT_ARG();
+ porttlv = parse_on_off("porttlv", *argv, &ret);
+ if (ret)
+ return ret;
+ } else if (strcmp(*argv, "porttlv-value") == 0) {
+ __u32 v;
+
+ NEXT_ARG();
+ if (get_u32(&v, *argv, 0) || v > 0xff)
+ invarg("invalid porttlv-value", *argv);
+ porttlv_value = v;
+ } else {
+ cfm_usage();
+ }
+ argc--; argv++;
+ }
+
+ if (!bridge_dev || !instance_set || !dmac || !period_set) {
+ fprintf(stderr,
+ "Required: bridge DEV instance INSTANCE dmac LLADDR period PERIOD\n");
+ return -1;
+ }
+
+ req.ifm.ifi_index = ll_name_to_index(bridge_dev);
+ if (!req.ifm.ifi_index)
+ return nodev(bridge_dev);
+
+ alen = ll_addr_a2n(abuf, sizeof(abuf), dmac);
+ if (alen != ETH_ALEN) {
+ fprintf(stderr, "Invalid address %s\n", dmac);
+ return -1;
+ }
+
+ afspec = addattr_nest(&req.n, sizeof(req), IFLA_AF_SPEC);
+ addattr16(&req.n, sizeof(req), IFLA_BRIDGE_FLAGS, BRIDGE_FLAGS_SELF);
+ cfm = addattr_nest(&req.n, sizeof(req), IFLA_BRIDGE_CFM | NLA_F_NESTED);
+ tx = addattr_nest(&req.n, sizeof(req),
+ IFLA_BRIDGE_CFM_CC_CCM_TX | NLA_F_NESTED);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CCM_TX_INSTANCE,
+ instance);
+ addattr_l(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CCM_TX_DMAC, abuf,
+ ETH_ALEN);
+ addattr32(&req.n, sizeof(req),
+ IFLA_BRIDGE_CFM_CC_CCM_TX_SEQ_NO_UPDATE, seq);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CCM_TX_PERIOD,
+ period);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CCM_TX_IF_TLV,
+ iftlv);
+ addattr8(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CCM_TX_IF_TLV_VALUE,
+ iftlv_value);
+ addattr32(&req.n, sizeof(req), IFLA_BRIDGE_CFM_CC_CCM_TX_PORT_TLV,
+ porttlv);
+ addattr8(&req.n, sizeof(req),
+ IFLA_BRIDGE_CFM_CC_CCM_TX_PORT_TLV_VALUE, porttlv_value);
+ addattr_nest_end(&req.n, tx);
+ addattr_nest_end(&req.n, cfm);
+ addattr_nest_end(&req.n, afspec);
+
+ return cfm_talk(&req.n);
+}
+
+static int cfm_cc(int argc, char **argv)
+{
+ if (argc <= 0)
+ cfm_usage();
+
+ if (strcmp(*argv, "config") == 0)
+ return cfm_cc_config(argc - 1, argv + 1);
+ if (strcmp(*argv, "peer") == 0) {
+ if (argc < 2)
+ cfm_usage();
+ if (strcmp(argv[1], "add") == 0)
+ return cfm_cc_peer(argc - 2, argv + 2, true);
+ if (strcmp(argv[1], "delete") == 0 ||
+ strcmp(argv[1], "del") == 0)
+ return cfm_cc_peer(argc - 2, argv + 2, false);
+ cfm_usage();
+ }
+ if (strcmp(*argv, "rdi") == 0)
+ return cfm_cc_rdi(argc - 1, argv + 1);
+ if (strcmp(*argv, "ccm-tx") == 0)
+ return cfm_cc_ccm_tx(argc - 1, argv + 1);
+
+ cfm_usage();
+ return -1;
+}
+
+/* "bridge cfm show { config | status }" */
+
+static __u32 cfm_get_u32(struct rtattr *tb[], int type)
+{
+ return tb[type] ? rta_getattr_u32(tb[type]) : 0;
+}
+
+static __u8 cfm_get_u8(struct rtattr *tb[], int type)
+{
+ return tb[type] ? rta_getattr_u8(tb[type]) : 0;
+}
+
+static void cfm_print_mac(struct rtattr *attr, const char *key,
+ const char *fmt)
+{
+ SPRINT_BUF(b);
+
+ if (!attr || RTA_PAYLOAD(attr) < ETH_ALEN)
+ return;
+ ll_addr_n2a(RTA_DATA(attr), ETH_ALEN, 0, b, sizeof(b));
+ print_string(PRINT_ANY, key, fmt, b);
+}
+
+static void cfm_print_mep_create(struct rtattr *attr)
+{
+ struct rtattr *tb[IFLA_BRIDGE_CFM_MEP_CREATE_MAX + 1];
+ const char *port;
+
+ parse_rtattr_nested(tb, IFLA_BRIDGE_CFM_MEP_CREATE_MAX, attr);
+
+ print_uint(PRINT_ANY, "instance", " mep %u",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_CREATE_INSTANCE));
+ print_string(PRINT_ANY, "domain", " domain %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_CREATE_DOMAIN) ==
+ BR_CFM_VLAN ? "vlan" : "port");
+ print_string(PRINT_ANY, "direction", " direction %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_CREATE_DIRECTION) ==
+ BR_CFM_MEP_DIRECTION_UP ? "up" : "down");
+ port = ll_index_to_name(cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_CREATE_IFINDEX));
+ print_string(PRINT_ANY, "port", " port %s", port);
+ print_nl();
+}
+
+static void cfm_print_mep_config(struct rtattr *attr)
+{
+ struct rtattr *tb[IFLA_BRIDGE_CFM_MEP_CONFIG_MAX + 1];
+
+ parse_rtattr_nested(tb, IFLA_BRIDGE_CFM_MEP_CONFIG_MAX, attr);
+
+ cfm_print_mac(tb[IFLA_BRIDGE_CFM_MEP_CONFIG_UNICAST_MAC], "address",
+ " address %s");
+ print_uint(PRINT_ANY, "level", " level %u",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_CONFIG_MDLEVEL));
+ print_uint(PRINT_ANY, "mepid", " mepid %u",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_CONFIG_MEPID));
+ print_nl();
+}
+
+static void cfm_print_cc_config(struct rtattr *attr)
+{
+ struct rtattr *tb[IFLA_BRIDGE_CFM_CC_CONFIG_MAX + 1];
+ char maid[CFM_MAID_LENGTH * 2 + 1] = "";
+
+ parse_rtattr_nested(tb, IFLA_BRIDGE_CFM_CC_CONFIG_MAX, attr);
+
+ open_json_object("cc");
+ print_on_off(PRINT_ANY, "enable", " cc enable %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_CONFIG_ENABLE));
+ print_string(PRINT_ANY, "interval", " interval %s",
+ cfm_interval_str(cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_CONFIG_EXP_INTERVAL)));
+ if (tb[IFLA_BRIDGE_CFM_CC_CONFIG_EXP_MAID])
+ hexstring_n2a(RTA_DATA(tb[IFLA_BRIDGE_CFM_CC_CONFIG_EXP_MAID]),
+ RTA_PAYLOAD(tb[IFLA_BRIDGE_CFM_CC_CONFIG_EXP_MAID]),
+ maid, sizeof(maid));
+ print_string(PRINT_ANY, "maid", " maid %s", maid);
+ print_nl();
+ close_json_object();
+}
+
+static void cfm_print_cc_rdi(struct rtattr *attr)
+{
+ struct rtattr *tb[IFLA_BRIDGE_CFM_CC_RDI_MAX + 1];
+
+ parse_rtattr_nested(tb, IFLA_BRIDGE_CFM_CC_RDI_MAX, attr);
+
+ print_on_off(PRINT_ANY, "rdi", " rdi %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_RDI_RDI));
+ print_nl();
+}
+
+static void cfm_print_cc_ccm_tx(struct rtattr *attr)
+{
+ struct rtattr *tb[IFLA_BRIDGE_CFM_CC_CCM_TX_MAX + 1];
+
+ parse_rtattr_nested(tb, IFLA_BRIDGE_CFM_CC_CCM_TX_MAX, attr);
+
+ open_json_object("ccm_tx");
+ cfm_print_mac(tb[IFLA_BRIDGE_CFM_CC_CCM_TX_DMAC], "dmac",
+ " ccm-tx dmac %s");
+ print_on_off(PRINT_ANY, "seq", " seq %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_CCM_TX_SEQ_NO_UPDATE));
+ print_uint(PRINT_ANY, "period", " period %u",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_CCM_TX_PERIOD));
+ print_on_off(PRINT_ANY, "iftlv", " iftlv %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_CCM_TX_IF_TLV));
+ print_uint(PRINT_ANY, "iftlv_value", " iftlv-value %u",
+ cfm_get_u8(tb, IFLA_BRIDGE_CFM_CC_CCM_TX_IF_TLV_VALUE));
+ print_on_off(PRINT_ANY, "porttlv", " porttlv %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_CCM_TX_PORT_TLV));
+ print_uint(PRINT_ANY, "porttlv_value", " porttlv-value %u",
+ cfm_get_u8(tb, IFLA_BRIDGE_CFM_CC_CCM_TX_PORT_TLV_VALUE));
+ print_nl();
+ close_json_object();
+}
+
+static void cfm_print_cc_peer(struct rtattr *attr)
+{
+ struct rtattr *tb[IFLA_BRIDGE_CFM_CC_PEER_MEP_MAX + 1];
+
+ parse_rtattr_nested(tb, IFLA_BRIDGE_CFM_CC_PEER_MEP_MAX, attr);
+
+ open_json_object(NULL);
+ print_uint(PRINT_ANY, "mepid", " peer mepid %u",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_PEER_MEPID));
+ print_nl();
+ close_json_object();
+}
+
+static void cfm_print_mep_status(struct rtattr *attr)
+{
+ struct rtattr *tb[IFLA_BRIDGE_CFM_MEP_STATUS_MAX + 1];
+
+ parse_rtattr_nested(tb, IFLA_BRIDGE_CFM_MEP_STATUS_MAX, attr);
+
+ print_uint(PRINT_ANY, "instance", " mep %u",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_STATUS_INSTANCE));
+ print_bool(PRINT_ANY, "opcode_unexp_seen", " opcode_unexp_seen %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_STATUS_OPCODE_UNEXP_SEEN));
+ print_bool(PRINT_ANY, "version_unexp_seen", " version_unexp_seen %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_STATUS_VERSION_UNEXP_SEEN));
+ print_bool(PRINT_ANY, "rx_level_low_seen", " rx_level_low_seen %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_MEP_STATUS_RX_LEVEL_LOW_SEEN));
+ print_nl();
+}
+
+static void cfm_print_cc_peer_status(struct rtattr *attr)
+{
+ struct rtattr *tb[IFLA_BRIDGE_CFM_CC_PEER_STATUS_MAX + 1];
+
+ parse_rtattr_nested(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_MAX, attr);
+
+ open_json_object(NULL);
+ print_uint(PRINT_ANY, "mepid", " peer mepid %u",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_PEER_MEPID));
+ print_bool(PRINT_ANY, "ccm_defect", " ccm_defect %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_CCM_DEFECT));
+ print_bool(PRINT_ANY, "rdi", " rdi %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_RDI));
+ print_uint(PRINT_ANY, "port_tlv_value", " port_tlv_value %u",
+ cfm_get_u8(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_PORT_TLV_VALUE));
+ print_uint(PRINT_ANY, "if_tlv_value", " if_tlv_value %u",
+ cfm_get_u8(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_IF_TLV_VALUE));
+ print_bool(PRINT_ANY, "seen", " seen %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_SEEN));
+ print_bool(PRINT_ANY, "tlv_seen", " tlv_seen %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_TLV_SEEN));
+ print_bool(PRINT_ANY, "seq_unexp_seen", " seq_unexp_seen %s",
+ cfm_get_u32(tb, IFLA_BRIDGE_CFM_CC_PEER_STATUS_SEQ_UNEXP_SEEN));
+ print_nl();
+ close_json_object();
+}
+
+/* The kernel sends one nest per MEP followed by its sub-nests and one
+ * nest per peer, so walk them in order and group by MEP.
+ */
+static int cfm_show_filter(struct nlmsghdr *n, void *arg)
+{
+ struct ifinfomsg *ifi = NLMSG_DATA(n);
+ int len = n->nlmsg_len - NLMSG_LENGTH(sizeof(*ifi));
+ bool status = !strcmp(arg, "status");
+ bool dev_open = false, mep_open = false, peers_open = false;
+ struct rtattr *afspec, *cfm, *a;
+
+ if (len < 0)
+ return -1;
+
+ if (n->nlmsg_type != RTM_NEWLINK || ifi->ifi_family != AF_BRIDGE)
+ return 0;
+
+ if (filter_index && filter_index != (unsigned int)ifi->ifi_index)
+ return 0;
+
+ afspec = parse_rtattr_one(IFLA_AF_SPEC, IFLA_RTA(ifi), len);
+ if (!afspec)
+ return 0;
+
+ cfm = parse_rtattr_one_nested(NLA_F_NESTED | IFLA_BRIDGE_CFM, afspec);
+ if (!cfm)
+ return 0;
+
+ rtattr_for_each_nested(a, cfm) {
+ unsigned short type = a->rta_type & NLA_TYPE_MASK;
+ bool new_mep, peer;
+
+ new_mep = type == (status ? IFLA_BRIDGE_CFM_MEP_STATUS_INFO :
+ IFLA_BRIDGE_CFM_MEP_CREATE_INFO);
+ peer = type == (status ? IFLA_BRIDGE_CFM_CC_PEER_STATUS_INFO :
+ IFLA_BRIDGE_CFM_CC_PEER_MEP_INFO);
+
+ if (!new_mep && !mep_open)
+ continue;
+
+ if (peers_open && !peer) {
+ close_json_array(PRINT_JSON, NULL);
+ peers_open = false;
+ }
+ if (new_mep) {
+ if (mep_open)
+ close_json_object();
+ if (!dev_open) {
+ open_json_object(NULL);
+ print_color_string(PRINT_ANY, COLOR_IFNAME,
+ "ifname", "%s\n",
+ ll_index_to_name(ifi->ifi_index));
+ open_json_array(PRINT_JSON, "meps");
+ dev_open = true;
+ }
+ open_json_object(NULL);
+ mep_open = true;
+ }
+ if (peer && !peers_open) {
+ open_json_array(PRINT_JSON, "peers");
+ peers_open = true;
+ }
+
+ switch (type) {
+ case IFLA_BRIDGE_CFM_MEP_CREATE_INFO:
+ cfm_print_mep_create(a);
+ break;
+ case IFLA_BRIDGE_CFM_MEP_CONFIG_INFO:
+ cfm_print_mep_config(a);
+ break;
+ case IFLA_BRIDGE_CFM_CC_CONFIG_INFO:
+ cfm_print_cc_config(a);
+ break;
+ case IFLA_BRIDGE_CFM_CC_RDI_INFO:
+ cfm_print_cc_rdi(a);
+ break;
+ case IFLA_BRIDGE_CFM_CC_CCM_TX_INFO:
+ cfm_print_cc_ccm_tx(a);
+ break;
+ case IFLA_BRIDGE_CFM_CC_PEER_MEP_INFO:
+ cfm_print_cc_peer(a);
+ break;
+ case IFLA_BRIDGE_CFM_MEP_STATUS_INFO:
+ cfm_print_mep_status(a);
+ break;
+ case IFLA_BRIDGE_CFM_CC_PEER_STATUS_INFO:
+ cfm_print_cc_peer_status(a);
+ break;
+ }
+ }
+
+ if (peers_open)
+ close_json_array(PRINT_JSON, NULL);
+ if (mep_open)
+ close_json_object();
+ if (dev_open) {
+ close_json_array(PRINT_JSON, NULL);
+ close_json_object();
+ }
+
+ return 0;
+}
+
+static int cfm_show(int argc, char **argv, const char *what)
+{
+ char *bridge_dev = NULL;
+ __u32 filt_mask;
+ int ret;
+
+ while (argc > 0) {
+ if (strcmp(*argv, "bridge") == 0) {
+ NEXT_ARG();
+ bridge_dev = *argv;
+ } else {
+ cfm_usage();
+ }
+ argc--; argv++;
+ }
+
+ if (bridge_dev) {
+ filter_index = ll_name_to_index(bridge_dev);
+ if (!filter_index)
+ return nodev(bridge_dev);
+ }
+
+ filt_mask = !strcmp(what, "config") ? RTEXT_FILTER_CFM_CONFIG :
+ RTEXT_FILTER_CFM_STATUS;
+
+ if (rtnl_linkdump_req_filter(&rth, PF_BRIDGE, filt_mask) < 0) {
+ fprintf(stderr, "Cannot send dump request\n");
+ return -1;
+ }
+
+ new_json_obj(json);
+ ret = rtnl_dump_filter(&rth, cfm_show_filter, (void *)what);
+ delete_json_obj();
+ if (ret < 0) {
+ fprintf(stderr, "Dump terminated\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+static int cfm_show_cmd(int argc, char **argv)
+{
+ if (argc <= 0)
+ cfm_usage();
+
+ if (strcmp(*argv, "config") == 0)
+ return cfm_show(argc - 1, argv + 1, "config");
+ if (strcmp(*argv, "status") == 0)
+ return cfm_show(argc - 1, argv + 1, "status");
+
+ cfm_usage();
+ return -1;
+}
+
+int do_cfm(int argc, char **argv)
+{
+ if (argc <= 0)
+ cfm_usage();
+
+ if (strcmp(*argv, "mep") == 0)
+ return cfm_mep(argc - 1, argv + 1);
+ if (strcmp(*argv, "cc") == 0)
+ return cfm_cc(argc - 1, argv + 1);
+ if (strcmp(*argv, "show") == 0)
+ return cfm_show_cmd(argc - 1, argv + 1);
+ if (strcmp(*argv, "help") == 0)
+ cfm_usage();
+
+ fprintf(stderr, "Command \"%s\" is unknown, try \"bridge cfm help\".\n",
+ *argv);
+ return -1;
+}
diff --git a/man/man8/bridge.8 b/man/man8/bridge.8
index d2323521..13b02bb3 100644
--- a/man/man8/bridge.8
+++ b/man/man8/bridge.8
@@ -13,7 +13,7 @@ bridge \- show / manipulate bridge addresses and devices
.ti -8
.IR OBJECT " := { "
-.BR link " | " fdb " | " mdb " | " vlan " | " vni " | " monitor " }"
+.BR link " | " fdb " | " mdb " | " vlan " | " vni " | " cfm " | " monitor " }"
.sp
.ti -8
@@ -1666,6 +1666,200 @@ option, the command displays per-vni traffic statistics.
.BI dev " NAME"
shows vni filtering table associated with the vxlan device
+.SH bridge cfm - Connectivity Fault Management
+
+.B cfm
+objects are IEEE 802.1ag Maintenance End Points (MEPs) on a bridge. A MEP
+sends Continuity Check Messages (CCMs) out of one bridge port and tracks
+the CCMs it receives from a list of peer MEPs. All commands take the
+bridge device, not the port.
+
+The kernel only supports port domain, down MEPs.
+
+.SS bridge cfm mep add - create a MEP
+
+.B bridge cfm mep add bridge
+.I DEV
+.B instance
+.I INSTANCE
+.B port
+.I PORT
+.RB "[ " domain " { " port " | " vlan " } ]"
+.RB "[ " direction " { " down " | " up " } ]"
+
+.TP
+.BI instance " INSTANCE"
+a number that identifies the MEP on this bridge.
+
+.TP
+.BI port " PORT"
+the bridge port the MEP sits on. Only one MEP per port.
+
+.TP
+.BI domain " { port | vlan }"
+the MEP domain. Default is
+.BR port ,
+the kernel rejects
+.BR vlan .
+
+.TP
+.BI direction " { down | up }"
+the MEP direction. Default is
+.BR down ,
+the kernel rejects
+.BR up .
+
+.SS bridge cfm mep del - delete a MEP
+
+.B bridge cfm mep del bridge
+.I DEV
+.B instance
+.I INSTANCE
+
+.SS bridge cfm mep set - set the MEP address, level and ID
+
+.B bridge cfm mep set bridge
+.I DEV
+.B instance
+.I INSTANCE
+.B address
+.I LLADDR
+.B level
+.I LEVEL
+.B mepid
+.I MEPID
+
+.TP
+.BI address " LLADDR"
+source MAC address of the CCMs this MEP sends.
+
+.TP
+.BI level " LEVEL"
+the MD level, 0 to 7.
+
+.TP
+.BI mepid " MEPID"
+the MEP ID, 0 to 8191.
+
+.SS bridge cfm cc config - configure Continuity Check
+
+.B bridge cfm cc config bridge
+.I DEV
+.B instance
+.I INSTANCE
+.B enable
+.RB "{ " on " | " off " }"
+.B interval
+.I INTERVAL
+.B maid
+.I MAID
+
+.TP
+.BI enable " { on | off }"
+turn CCM reception and peer tracking on or off.
+
+.TP
+.BI interval " INTERVAL"
+the CCM interval, one of
+.BR 3ms3 ", " 10ms ", " 100ms ", " 1s ", " 10s ", " 1m ", " 10m .
+It is used both for sending and for the peer loss timeout. The kernel runs
+CCMs from a workqueue, so the real interval is rounded up to jiffies and
+can be noticeably longer than the name, most of all for
+.BR 3ms3 .
+
+.TP
+.BI maid " MAID"
+the Maintenance Association ID as a hex string, up to 48 bytes. Shorter
+strings are padded with zeros. Received CCMs must carry the same MAID.
+
+.SS bridge cfm cc peer - add or delete a peer MEP
+
+.B bridge cfm cc peer
+.RB "{ " add " | " del " }"
+.B bridge
+.I DEV
+.B instance
+.I INSTANCE
+.B mepid
+.I MEPID
+
+CCMs from MEP IDs not in this list are not tracked.
+
+.SS bridge cfm cc rdi - set the RDI bit
+
+.B bridge cfm cc rdi bridge
+.I DEV
+.B instance
+.I INSTANCE
+.B rdi
+.RB "{ " on " | " off " }"
+
+Sets or clears the Remote Defect Indication bit in the CCMs this MEP sends.
+
+.SS bridge cfm cc ccm-tx - send CCMs
+
+.B bridge cfm cc ccm-tx bridge
+.I DEV
+.B instance
+.I INSTANCE
+.B dmac
+.I LLADDR
+.B period
+.I PERIOD
+.RB "[ " seq " { " on " | " off " } ]"
+.RB "[ " iftlv " { " on " | " off " } ]"
+.RB "[ " iftlv-value
+.IR VALUE " ]"
+.RB "[ " porttlv " { " on " | " off " } ]"
+.RB "[ " porttlv-value
+.IR VALUE " ]"
+
+.TP
+.BI dmac " LLADDR"
+destination MAC address of the CCMs, usually
+.BR 01:80:c2:00:00:3X
+where X is the MD level.
+
+.TP
+.BI period " PERIOD"
+how many seconds to keep sending. The kernel stops by itself after that,
+so run the command again to keep going.
+.B 0
+stops sending.
+
+.TP
+.BI seq " { on | off }"
+put an incrementing sequence number in each CCM. Default
+.BR off ,
+which sends 0.
+
+.TP
+.BI iftlv " { on | off }"
+add an Interface Status TLV with value
+.BR iftlv-value .
+
+.TP
+.BI porttlv " { on | off }"
+add a Port Status TLV with value
+.BR porttlv-value .
+
+.SS bridge cfm show - show configuration or status
+
+.B bridge cfm show
+.RB "{ " config " | " status " }"
+.RB "[ " bridge
+.IR DEV " ]"
+
+.B config
+shows every MEP with its CC, RDI, CCM transmit settings and peers.
+.B status
+shows the MEP and peer state. The
+.BR seen ", " tlv_seen ", " seq_unexp_seen
+and the MEP
+.B _seen
+flags are cleared by the kernel each time status is read, so they mean
+"since the last read".
+
.SH bridge monitor - state monitoring
The
--
2.53.0
next prev parent reply other threads:[~2026-10-04 4:13 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-04 4:13 [PATCH iproute2-next 0/2] bridge: add cfm command Abdul Wasey
2026-10-04 4:13 ` [PATCH iproute2-next 1/2] uapi: import cfm_bridge.h Abdul Wasey
2026-10-04 4:13 ` Abdul Wasey [this message]
2026-10-06 10:42 ` [PATCH iproute2-next 0/2] bridge: add cfm command Ido Schimmel
2026-10-06 13:44 ` Abdul Wasey
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261004041307.2683990-3-w453y.me@gmail.com \
--to=w453y.me@gmail.com \
--cc=bridge@lists.linux.dev \
--cc=dsahern@kernel.org \
--cc=horatiu.vultur@microchip.com \
--cc=idosch@nvidia.com \
--cc=netdev@vger.kernel.org \
--cc=razor@blackwall.org \
--cc=stephen@networkplumber.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox