| /* |
| * netlink.c - basic infrastructure for netlink code |
| * |
| * Heart of the netlink interface implementation. |
| */ |
| |
| #include <errno.h> |
| |
| #include "../internal.h" |
| #include "netlink.h" |
| #include "extapi.h" |
| #include "msgbuff.h" |
| #include "nlsock.h" |
| #include "strset.h" |
| |
| /* Used as reply callback for requests where no reply is expected (e.g. most |
| * "set" type commands) |
| */ |
| int nomsg_reply_cb(const struct nlmsghdr *nlhdr, void *data __maybe_unused) |
| { |
| const struct genlmsghdr *ghdr = (const struct genlmsghdr *)(nlhdr + 1); |
| |
| fprintf(stderr, "received unexpected message: len=%u type=%u cmd=%u\n", |
| nlhdr->nlmsg_len, nlhdr->nlmsg_type, ghdr->cmd); |
| return MNL_CB_OK; |
| } |
| |
| /* standard attribute parser callback; it fills provided array with pointers |
| * to attributes like kernel nla_parse(). We must expect to run on top of |
| * a newer kernel which may send attributes that we do not know (yet). Rather |
| * than treating them as an error, just ignore them. |
| */ |
| int attr_cb(const struct nlattr *attr, void *data) |
| { |
| const struct attr_tb_info *tb_info = data; |
| uint16_t type = mnl_attr_get_type(attr); |
| |
| if (type <= tb_info->max_type) |
| tb_info->tb[type] = attr; |
| |
| return MNL_CB_OK; |
| } |
| |
| /* misc helpers */ |
| |
| const char *get_dev_name(const struct nlattr *nest) |
| { |
| const struct nlattr *tb[ETHTOOL_A_HEADER_MAX + 1] = {}; |
| DECLARE_ATTR_TB_INFO(tb); |
| int ret; |
| |
| if (!nest) |
| return NULL; |
| ret = mnl_attr_parse_nested(nest, attr_cb, &tb_info); |
| if (ret < 0 || !tb[ETHTOOL_A_HEADER_DEV_NAME]) |
| return "(none)"; |
| return mnl_attr_get_str(tb[ETHTOOL_A_HEADER_DEV_NAME]); |
| } |
| |
| int get_dev_info(const struct nlattr *nest, int *ifindex, char *ifname) |
| { |
| const struct nlattr *tb[ETHTOOL_A_HEADER_MAX + 1] = {}; |
| const struct nlattr *index_attr; |
| const struct nlattr *name_attr; |
| DECLARE_ATTR_TB_INFO(tb); |
| int ret; |
| |
| if (ifindex) |
| *ifindex = 0; |
| if (ifname) |
| memset(ifname, '\0', ALTIFNAMSIZ); |
| |
| if (!nest) |
| return -EFAULT; |
| ret = mnl_attr_parse_nested(nest, attr_cb, &tb_info); |
| index_attr = tb[ETHTOOL_A_HEADER_DEV_INDEX]; |
| name_attr = tb[ETHTOOL_A_HEADER_DEV_NAME]; |
| if (ret < 0 || (ifindex && !index_attr) || (ifname && !name_attr)) |
| return -EFAULT; |
| |
| if (ifindex) |
| *ifindex = mnl_attr_get_u32(index_attr); |
| if (ifname) { |
| strncpy(ifname, mnl_attr_get_str(name_attr), ALTIFNAMSIZ); |
| if (ifname[ALTIFNAMSIZ - 1]) { |
| ifname[ALTIFNAMSIZ - 1] = '\0'; |
| fprintf(stderr, "kernel device name too long: '%s'\n", |
| mnl_attr_get_str(name_attr)); |
| return -EFAULT; |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * netlink_cmd_check() - check support for netlink command |
| * @ctx: ethtool command context |
| * @cmd: netlink command id |
| * @devname: device name from user |
| * @allow_wildcard: wildcard dumps supported |
| * |
| * Check if command @cmd is known to be unsupported based on ops information |
| * from genetlink family id request. Set nlctx->ioctl_fallback if ethtool |
| * should fall back to ioctl, i.e. when we do not know in advance that |
| * netlink request is supported. Set nlctx->wildcard_unsupported if "*" was |
| * used as device name but the request does not support wildcards (on either |
| * side). |
| * |
| * Return: true if we know the netlink request is not supported and should |
| * fail (and possibly fall back) without actually sending it to kernel. |
| */ |
| bool netlink_cmd_check(struct cmd_context *ctx, unsigned int cmd, |
| bool allow_wildcard) |
| { |
| bool is_dump = !strcmp(ctx->devname, WILDCARD_DEVNAME); |
| uint32_t cap = is_dump ? GENL_CMD_CAP_DUMP : GENL_CMD_CAP_DO; |
| struct nl_context *nlctx = ctx->nlctx; |
| |
| if (is_dump && !allow_wildcard) { |
| nlctx->wildcard_unsupported = true; |
| return true; |
| } |
| if (!nlctx->ops_info) { |
| nlctx->ioctl_fallback = true; |
| return false; |
| } |
| if (cmd > ETHTOOL_MSG_USER_MAX || !nlctx->ops_info[cmd].op_flags) { |
| nlctx->ioctl_fallback = true; |
| return true; |
| } |
| |
| if (is_dump && !(nlctx->ops_info[cmd].op_flags & GENL_CMD_CAP_DUMP)) |
| nlctx->wildcard_unsupported = true; |
| |
| return !(nlctx->ops_info[cmd].op_flags & cap); |
| } |
| |
| struct ethtool_op_policy_query_ctx { |
| struct nl_context *nlctx; |
| unsigned int op; |
| unsigned int op_hdr_attr; |
| |
| bool op_policy_found; |
| bool hdr_policy_found; |
| unsigned int op_policy_idx; |
| unsigned int hdr_policy_idx; |
| uint64_t flag_mask; |
| }; |
| |
| static int family_policy_find_op(struct ethtool_op_policy_query_ctx *policy_ctx, |
| const struct nlattr *op_policy) |
| { |
| const struct nlattr *attr; |
| unsigned int type; |
| int ret; |
| |
| type = policy_ctx->nlctx->is_dump ? |
| CTRL_ATTR_POLICY_DUMP : CTRL_ATTR_POLICY_DO; |
| |
| mnl_attr_for_each_nested(attr, op_policy) { |
| const struct nlattr *tb[CTRL_ATTR_POLICY_DUMP_MAX + 1] = {}; |
| DECLARE_ATTR_TB_INFO(tb); |
| |
| if (mnl_attr_get_type(attr) != policy_ctx->op) |
| continue; |
| |
| ret = mnl_attr_parse_nested(attr, attr_cb, &tb_info); |
| if (ret < 0) |
| return ret; |
| |
| if (!tb[type]) |
| continue; |
| |
| policy_ctx->op_policy_found = true; |
| policy_ctx->op_policy_idx = mnl_attr_get_u32(tb[type]); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static int family_policy_cb(const struct nlmsghdr *nlhdr, void *data) |
| { |
| const struct nlattr *tba[NL_POLICY_TYPE_ATTR_MAX + 1] = {}; |
| DECLARE_ATTR_TB_INFO(tba); |
| const struct nlattr *tb[CTRL_ATTR_MAX + 1] = {}; |
| DECLARE_ATTR_TB_INFO(tb); |
| struct ethtool_op_policy_query_ctx *policy_ctx = data; |
| const struct nlattr *policy_attr, *attr_attr, *attr; |
| unsigned int attr_idx, policy_idx; |
| int ret; |
| |
| ret = mnl_attr_parse(nlhdr, GENL_HDRLEN, attr_cb, &tb_info); |
| if (ret < 0) |
| return MNL_CB_ERROR; |
| |
| if (!policy_ctx->op_policy_found) { |
| if (!tb[CTRL_ATTR_OP_POLICY]) { |
| fprintf(stderr, "Error: op policy map not present\n"); |
| return MNL_CB_ERROR; |
| } |
| ret = family_policy_find_op(policy_ctx, tb[CTRL_ATTR_OP_POLICY]); |
| return ret < 0 ? MNL_CB_ERROR : MNL_CB_OK; |
| } |
| |
| if (!tb[CTRL_ATTR_POLICY]) |
| return MNL_CB_OK; |
| |
| policy_attr = mnl_attr_get_payload(tb[CTRL_ATTR_POLICY]); |
| policy_idx = mnl_attr_get_type(policy_attr); |
| attr_attr = mnl_attr_get_payload(policy_attr); |
| attr_idx = mnl_attr_get_type(attr_attr); |
| |
| ret = mnl_attr_parse_nested(attr_attr, attr_cb, &tba_info); |
| if (ret < 0) |
| return MNL_CB_ERROR; |
| |
| if (policy_idx == policy_ctx->op_policy_idx && |
| attr_idx == policy_ctx->op_hdr_attr) { |
| attr = tba[NL_POLICY_TYPE_ATTR_POLICY_IDX]; |
| if (!attr) { |
| fprintf(stderr, "Error: no policy index in what was expected to be ethtool header attribute\n"); |
| return MNL_CB_ERROR; |
| } |
| policy_ctx->hdr_policy_found = true; |
| policy_ctx->hdr_policy_idx = mnl_attr_get_u32(attr); |
| } |
| |
| if (policy_ctx->hdr_policy_found && |
| policy_ctx->hdr_policy_idx == policy_idx && |
| attr_idx == ETHTOOL_A_HEADER_FLAGS) { |
| attr = tba[NL_POLICY_TYPE_ATTR_MASK]; |
| if (!attr) { |
| fprintf(stderr, "Error: validation mask not reported for ethtool header flags\n"); |
| return MNL_CB_ERROR; |
| } |
| |
| policy_ctx->flag_mask = mnl_attr_get_u64(attr); |
| } |
| |
| return MNL_CB_OK; |
| } |
| |
| static int read_flags_policy(struct nl_context *nlctx, struct nl_socket *nlsk, |
| unsigned int nlcmd, unsigned int hdrattr) |
| { |
| struct ethtool_op_policy_query_ctx policy_ctx; |
| struct nl_msg_buff *msgbuff = &nlsk->msgbuff; |
| int ret; |
| |
| if (nlctx->ops_info[nlcmd].hdr_policy_loaded) |
| return 0; |
| |
| memset(&policy_ctx, 0, sizeof(policy_ctx)); |
| policy_ctx.nlctx = nlctx; |
| policy_ctx.op = nlcmd; |
| policy_ctx.op_hdr_attr = hdrattr; |
| |
| ret = __msg_init(msgbuff, GENL_ID_CTRL, CTRL_CMD_GETPOLICY, |
| NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP, 1); |
| if (ret < 0) |
| return ret; |
| ret = -EMSGSIZE; |
| if (ethnla_put_u16(msgbuff, CTRL_ATTR_FAMILY_ID, nlctx->ethnl_fam)) |
| return ret; |
| if (ethnla_put_u32(msgbuff, CTRL_ATTR_OP, nlcmd)) |
| return ret; |
| |
| nlsock_sendmsg(nlsk, NULL); |
| nlsock_process_reply(nlsk, family_policy_cb, &policy_ctx); |
| |
| nlctx->ops_info[nlcmd].hdr_policy_loaded = 1; |
| nlctx->ops_info[nlcmd].hdr_flags = policy_ctx.flag_mask; |
| return 0; |
| } |
| |
| u32 get_stats_flag(struct nl_context *nlctx, unsigned int nlcmd, |
| unsigned int hdrattr) |
| { |
| if (!nlctx->ctx->show_stats) |
| return 0; |
| if (nlcmd > ETHTOOL_MSG_USER_MAX || |
| !(nlctx->ops_info[nlcmd].op_flags & GENL_CMD_CAP_HASPOL)) |
| return 0; |
| |
| if (read_flags_policy(nlctx, nlctx->ethnl_socket, nlcmd, hdrattr) < 0) |
| return 0; |
| |
| return nlctx->ops_info[nlcmd].hdr_flags & ETHTOOL_FLAG_STATS; |
| } |
| |
| /* initialization */ |
| |
| static int genl_read_ops(struct nl_context *nlctx, |
| const struct nlattr *ops_attr) |
| { |
| struct nl_op_info *ops_info; |
| struct nlattr *op_attr; |
| int ret; |
| |
| ops_info = calloc(__ETHTOOL_MSG_USER_CNT, sizeof(ops_info[0])); |
| if (!ops_info) |
| return -ENOMEM; |
| |
| mnl_attr_for_each_nested(op_attr, ops_attr) { |
| const struct nlattr *tb[CTRL_ATTR_OP_MAX + 1] = {}; |
| DECLARE_ATTR_TB_INFO(tb); |
| uint32_t op_id; |
| |
| ret = mnl_attr_parse_nested(op_attr, attr_cb, &tb_info); |
| if (ret < 0) |
| goto err; |
| |
| if (!tb[CTRL_ATTR_OP_ID] || !tb[CTRL_ATTR_OP_FLAGS]) |
| continue; |
| op_id = mnl_attr_get_u32(tb[CTRL_ATTR_OP_ID]); |
| if (op_id >= __ETHTOOL_MSG_USER_CNT) |
| continue; |
| |
| ops_info[op_id].op_flags = |
| mnl_attr_get_u32(tb[CTRL_ATTR_OP_FLAGS]); |
| } |
| |
| nlctx->ops_info = ops_info; |
| return 0; |
| err: |
| free(ops_info); |
| return ret; |
| } |
| |
| static void find_mc_group(struct nl_context *nlctx, struct nlattr *nest) |
| { |
| const struct nlattr *grp_tb[CTRL_ATTR_MCAST_GRP_MAX + 1] = {}; |
| DECLARE_ATTR_TB_INFO(grp_tb); |
| struct nlattr *grp_attr; |
| int ret; |
| |
| mnl_attr_for_each_nested(grp_attr, nest) { |
| ret = mnl_attr_parse_nested(grp_attr, attr_cb, &grp_tb_info); |
| if (ret < 0) |
| return; |
| if (!grp_tb[CTRL_ATTR_MCAST_GRP_NAME] || |
| !grp_tb[CTRL_ATTR_MCAST_GRP_ID]) |
| continue; |
| if (strcmp(mnl_attr_get_str(grp_tb[CTRL_ATTR_MCAST_GRP_NAME]), |
| ETHTOOL_MCGRP_MONITOR_NAME)) |
| continue; |
| nlctx->ethnl_mongrp = |
| mnl_attr_get_u32(grp_tb[CTRL_ATTR_MCAST_GRP_ID]); |
| return; |
| } |
| } |
| |
| static int __maybe_unused family_info_cb(const struct nlmsghdr *nlhdr, |
| void *data) |
| { |
| struct nl_context *nlctx = data; |
| struct nlattr *attr; |
| int ret; |
| |
| mnl_attr_for_each(attr, nlhdr, GENL_HDRLEN) { |
| switch (mnl_attr_get_type(attr)) { |
| case CTRL_ATTR_FAMILY_ID: |
| nlctx->ethnl_fam = mnl_attr_get_u16(attr); |
| break; |
| case CTRL_ATTR_OPS: |
| ret = genl_read_ops(nlctx, attr); |
| if (ret < 0) |
| return MNL_CB_ERROR; |
| break; |
| case CTRL_ATTR_MCAST_GROUPS: |
| find_mc_group(nlctx, attr); |
| break; |
| } |
| } |
| |
| return MNL_CB_OK; |
| } |
| |
| #ifdef TEST_ETHTOOL |
| static int get_genl_family(struct nl_context *nlctx __maybe_unused, |
| struct nl_socket *nlsk __maybe_unused) |
| { |
| return 0; |
| } |
| #else |
| static int get_genl_family(struct nl_context *nlctx, struct nl_socket *nlsk) |
| { |
| struct nl_msg_buff *msgbuff = &nlsk->msgbuff; |
| int ret; |
| |
| nlctx->suppress_nlerr = 2; |
| ret = __msg_init(msgbuff, GENL_ID_CTRL, CTRL_CMD_GETFAMILY, |
| NLM_F_REQUEST | NLM_F_ACK, 1); |
| if (ret < 0) |
| goto out; |
| ret = -EMSGSIZE; |
| if (ethnla_put_strz(msgbuff, CTRL_ATTR_FAMILY_NAME, ETHTOOL_GENL_NAME)) |
| goto out; |
| |
| nlsock_sendmsg(nlsk, NULL); |
| nlsock_process_reply(nlsk, family_info_cb, nlctx); |
| ret = nlctx->ethnl_fam ? 0 : -EADDRNOTAVAIL; |
| |
| out: |
| nlctx->suppress_nlerr = 0; |
| return ret; |
| } |
| #endif |
| |
| int netlink_init(struct cmd_context *ctx) |
| { |
| struct nl_context *nlctx; |
| int ret; |
| |
| nlctx = calloc(1, sizeof(*nlctx)); |
| if (!nlctx) |
| return -ENOMEM; |
| nlctx->ctx = ctx; |
| ret = nlsock_init(nlctx, &nlctx->ethnl_socket, NETLINK_GENERIC); |
| if (ret < 0) |
| goto out_free; |
| ret = get_genl_family(nlctx, nlctx->ethnl_socket); |
| if (ret < 0) |
| goto out_nlsk; |
| |
| ctx->nlctx = nlctx; |
| return 0; |
| |
| out_nlsk: |
| nlsock_done(nlctx->ethnl_socket); |
| out_free: |
| free(nlctx->ops_info); |
| free(nlctx); |
| return ret; |
| } |
| |
| static void netlink_done(struct cmd_context *ctx) |
| { |
| struct nl_context *nlctx = ctx->nlctx; |
| |
| if (!nlctx) |
| return; |
| |
| nlsock_done(nlctx->ethnl_socket); |
| nlsock_done(nlctx->ethnl2_socket); |
| nlsock_done(nlctx->rtnl_socket); |
| free(nlctx->ops_info); |
| free(nlctx); |
| ctx->nlctx = NULL; |
| cleanup_all_strings(); |
| } |
| |
| /** |
| * netlink_run_handler() - run netlink handler for subcommand |
| * @ctx: command context |
| * @nlchk: netlink capability check |
| * @nlfunc: subcommand netlink handler to call |
| * @no_fallback: there is no ioctl fallback handler |
| * |
| * This function returns only if ioctl() handler should be run as fallback. |
| * Otherwise it exits with appropriate return code. |
| */ |
| void netlink_run_handler(struct cmd_context *ctx, nl_chk_t nlchk, |
| nl_func_t nlfunc, bool no_fallback) |
| { |
| bool wildcard = ctx->devname && !strcmp(ctx->devname, WILDCARD_DEVNAME); |
| bool wildcard_unsupported, ioctl_fallback; |
| struct nl_context *nlctx; |
| const char *reason; |
| int ret; |
| |
| if (ctx->nl_disable) { |
| reason = "netlink disabled"; |
| goto no_support; |
| } |
| |
| if (nlchk && !nlchk(ctx)) { |
| reason = "ioctl-only request"; |
| goto no_support; |
| } |
| if (ctx->devname && strlen(ctx->devname) >= ALTIFNAMSIZ) { |
| fprintf(stderr, "device name '%s' longer than %u characters\n", |
| ctx->devname, ALTIFNAMSIZ - 1); |
| exit(1); |
| } |
| |
| if (!nlfunc) { |
| reason = "ethtool netlink support for subcommand missing"; |
| goto no_support; |
| } |
| if (netlink_init(ctx)) { |
| reason = "netlink interface initialization failed"; |
| goto no_support; |
| } |
| nlctx = ctx->nlctx; |
| |
| ret = nlfunc(ctx); |
| wildcard_unsupported = nlctx->wildcard_unsupported; |
| ioctl_fallback = nlctx->ioctl_fallback; |
| netlink_done(ctx); |
| |
| if (no_fallback || ret != -EOPNOTSUPP || !ioctl_fallback) { |
| if (wildcard_unsupported) |
| fprintf(stderr, "%s\n", |
| "subcommand does not support wildcard dump"); |
| exit(ret >= 0 ? ret : 1); |
| } |
| if (wildcard_unsupported) |
| reason = "subcommand does not support wildcard dump"; |
| else |
| reason = "kernel netlink support for subcommand missing"; |
| |
| no_support: |
| if (no_fallback) { |
| fprintf(stderr, "%s, subcommand not supported by ioctl\n", |
| reason); |
| exit(1); |
| } |
| if (wildcard) { |
| fprintf(stderr, "%s, wildcard dump not supported\n", reason); |
| exit(1); |
| } |
| if (ctx->devname && strlen(ctx->devname) >= IFNAMSIZ) { |
| fprintf(stderr, |
| "%s, device name longer than %u not supported\n", |
| reason, IFNAMSIZ - 1); |
| exit(1); |
| } |
| |
| /* fallback to ioctl() */ |
| } |