| // SPDX-License-Identifier: GPL-2.0-only | |
| #include <linux/ethtool_netlink.h> | |
| #include <net/udp_tunnel.h> | |
| #include <net/vxlan.h> | |
| #include "bitset.h" | |
| #include "common.h" | |
| #include "netlink.h" | |
| const struct nla_policy ethnl_tunnel_info_get_policy[] = { | |
| [ETHTOOL_A_TUNNEL_INFO_HEADER] = | |
| NLA_POLICY_NESTED(ethnl_header_policy), | |
| }; | |
| static_assert(ETHTOOL_UDP_TUNNEL_TYPE_VXLAN == ilog2(UDP_TUNNEL_TYPE_VXLAN)); | |
| static_assert(ETHTOOL_UDP_TUNNEL_TYPE_GENEVE == ilog2(UDP_TUNNEL_TYPE_GENEVE)); | |
| static_assert(ETHTOOL_UDP_TUNNEL_TYPE_VXLAN_GPE == | |
| ilog2(UDP_TUNNEL_TYPE_VXLAN_GPE)); | |
| static ssize_t ethnl_udp_table_reply_size(unsigned int types, bool compact) | |
| { | |
| ssize_t size; | |
| size = ethnl_bitset32_size(&types, NULL, __ETHTOOL_UDP_TUNNEL_TYPE_CNT, | |
| udp_tunnel_type_names, compact); | |
| if (size < 0) | |
| return size; | |
| return size + | |
| nla_total_size(0) + /* _UDP_TABLE */ | |
| nla_total_size(sizeof(u32)); /* _UDP_TABLE_SIZE */ | |
| } | |
| static ssize_t | |
| ethnl_tunnel_info_reply_size(const struct ethnl_req_info *req_base, | |
| struct netlink_ext_ack *extack) | |
| { | |
| bool compact = req_base->flags & ETHTOOL_FLAG_COMPACT_BITSETS; | |
| const struct udp_tunnel_nic_info *info; | |
| unsigned int i; | |
| ssize_t ret; | |
| size_t size; | |
| info = req_base->dev->udp_tunnel_nic_info; | |
| if (!info) { | |
| NL_SET_ERR_MSG(extack, | |
| "device does not report tunnel offload info"); | |
| return -EOPNOTSUPP; | |
| } | |
| size = nla_total_size(0); /* _INFO_UDP_PORTS */ | |
| for (i = 0; i < UDP_TUNNEL_NIC_MAX_TABLES; i++) { | |
| if (!info->tables[i].n_entries) | |
| break; | |
| ret = ethnl_udp_table_reply_size(info->tables[i].tunnel_types, | |
| compact); | |
| if (ret < 0) | |
| return ret; | |
| size += ret; | |
| size += udp_tunnel_nic_dump_size(req_base->dev, i); | |
| } | |
| if (info->flags & UDP_TUNNEL_NIC_INFO_STATIC_IANA_VXLAN) { | |
| ret = ethnl_udp_table_reply_size(0, compact); | |
| if (ret < 0) | |
| return ret; | |
| size += ret; | |
| size += nla_total_size(0) + /* _TABLE_ENTRY */ | |
| nla_total_size(sizeof(__be16)) + /* _ENTRY_PORT */ | |
| nla_total_size(sizeof(u32)); /* _ENTRY_TYPE */ | |
| } | |
| return size; | |
| } | |
| static int | |
| ethnl_tunnel_info_fill_reply(const struct ethnl_req_info *req_base, | |
| struct sk_buff *skb) | |
| { | |
| bool compact = req_base->flags & ETHTOOL_FLAG_COMPACT_BITSETS; | |
| const struct udp_tunnel_nic_info *info; | |
| struct nlattr *ports, *table, *entry; | |
| unsigned int i; | |
| info = req_base->dev->udp_tunnel_nic_info; | |
| if (!info) | |
| return -EOPNOTSUPP; | |
| ports = nla_nest_start(skb, ETHTOOL_A_TUNNEL_INFO_UDP_PORTS); | |
| if (!ports) | |
| return -EMSGSIZE; | |
| for (i = 0; i < UDP_TUNNEL_NIC_MAX_TABLES; i++) { | |
| if (!info->tables[i].n_entries) | |
| break; | |
| table = nla_nest_start(skb, ETHTOOL_A_TUNNEL_UDP_TABLE); | |
| if (!table) | |
| goto err_cancel_ports; | |
| if (nla_put_u32(skb, ETHTOOL_A_TUNNEL_UDP_TABLE_SIZE, | |
| info->tables[i].n_entries)) | |
| goto err_cancel_table; | |
| if (ethnl_put_bitset32(skb, ETHTOOL_A_TUNNEL_UDP_TABLE_TYPES, | |
| &info->tables[i].tunnel_types, NULL, | |
| __ETHTOOL_UDP_TUNNEL_TYPE_CNT, | |
| udp_tunnel_type_names, compact)) | |
| goto err_cancel_table; | |
| if (udp_tunnel_nic_dump_write(req_base->dev, i, skb)) | |
| goto err_cancel_table; | |
| nla_nest_end(skb, table); | |
| } | |
| if (info->flags & UDP_TUNNEL_NIC_INFO_STATIC_IANA_VXLAN) { | |
| u32 zero = 0; | |
| table = nla_nest_start(skb, ETHTOOL_A_TUNNEL_UDP_TABLE); | |
| if (!table) | |
| goto err_cancel_ports; | |
| if (nla_put_u32(skb, ETHTOOL_A_TUNNEL_UDP_TABLE_SIZE, 1)) | |
| goto err_cancel_table; | |
| if (ethnl_put_bitset32(skb, ETHTOOL_A_TUNNEL_UDP_TABLE_TYPES, | |
| &zero, NULL, | |
| __ETHTOOL_UDP_TUNNEL_TYPE_CNT, | |
| udp_tunnel_type_names, compact)) | |
| goto err_cancel_table; | |
| entry = nla_nest_start(skb, ETHTOOL_A_TUNNEL_UDP_TABLE_ENTRY); | |
| if (!entry) | |
| goto err_cancel_entry; | |
| if (nla_put_be16(skb, ETHTOOL_A_TUNNEL_UDP_ENTRY_PORT, | |
| htons(IANA_VXLAN_UDP_PORT)) || | |
| nla_put_u32(skb, ETHTOOL_A_TUNNEL_UDP_ENTRY_TYPE, | |
| ilog2(UDP_TUNNEL_TYPE_VXLAN))) | |
| goto err_cancel_entry; | |
| nla_nest_end(skb, entry); | |
| nla_nest_end(skb, table); | |
| } | |
| nla_nest_end(skb, ports); | |
| return 0; | |
| err_cancel_entry: | |
| nla_nest_cancel(skb, entry); | |
| err_cancel_table: | |
| nla_nest_cancel(skb, table); | |
| err_cancel_ports: | |
| nla_nest_cancel(skb, ports); | |
| return -EMSGSIZE; | |
| } | |
| int ethnl_tunnel_info_doit(struct sk_buff *skb, struct genl_info *info) | |
| { | |
| struct ethnl_req_info req_info = {}; | |
| struct nlattr **tb = info->attrs; | |
| struct sk_buff *rskb; | |
| void *reply_payload; | |
| int reply_len; | |
| int ret; | |
| ret = ethnl_parse_header_dev_get(&req_info, | |
| tb[ETHTOOL_A_TUNNEL_INFO_HEADER], | |
| genl_info_net(info), info->extack, | |
| true); | |
| if (ret < 0) | |
| return ret; | |
| rtnl_lock(); | |
| ret = ethnl_tunnel_info_reply_size(&req_info, info->extack); | |
| if (ret < 0) | |
| goto err_unlock_rtnl; | |
| reply_len = ret + ethnl_reply_header_size(); | |
| rskb = ethnl_reply_init(reply_len, req_info.dev, | |
| ETHTOOL_MSG_TUNNEL_INFO_GET_REPLY, | |
| ETHTOOL_A_TUNNEL_INFO_HEADER, | |
| info, &reply_payload); | |
| if (!rskb) { | |
| ret = -ENOMEM; | |
| goto err_unlock_rtnl; | |
| } | |
| ret = ethnl_tunnel_info_fill_reply(&req_info, rskb); | |
| if (ret) | |
| goto err_free_msg; | |
| rtnl_unlock(); | |
| ethnl_parse_header_dev_put(&req_info); | |
| genlmsg_end(rskb, reply_payload); | |
| return genlmsg_reply(rskb, info); | |
| err_free_msg: | |
| nlmsg_free(rskb); | |
| err_unlock_rtnl: | |
| rtnl_unlock(); | |
| ethnl_parse_header_dev_put(&req_info); | |
| return ret; | |
| } | |
| struct ethnl_tunnel_info_dump_ctx { | |
| struct ethnl_req_info req_info; | |
| unsigned long ifindex; | |
| }; | |
| int ethnl_tunnel_info_start(struct netlink_callback *cb) | |
| { | |
| const struct genl_dumpit_info *info = genl_dumpit_info(cb); | |
| struct ethnl_tunnel_info_dump_ctx *ctx = (void *)cb->ctx; | |
| struct nlattr **tb = info->info.attrs; | |
| int ret; | |
| BUILD_BUG_ON(sizeof(*ctx) > sizeof(cb->ctx)); | |
| memset(ctx, 0, sizeof(*ctx)); | |
| ret = ethnl_parse_header_dev_get(&ctx->req_info, | |
| tb[ETHTOOL_A_TUNNEL_INFO_HEADER], | |
| sock_net(cb->skb->sk), cb->extack, | |
| false); | |
| if (ctx->req_info.dev) { | |
| ethnl_parse_header_dev_put(&ctx->req_info); | |
| ctx->req_info.dev = NULL; | |
| } | |
| return ret; | |
| } | |
| int ethnl_tunnel_info_dumpit(struct sk_buff *skb, struct netlink_callback *cb) | |
| { | |
| struct ethnl_tunnel_info_dump_ctx *ctx = (void *)cb->ctx; | |
| struct net *net = sock_net(skb->sk); | |
| struct net_device *dev; | |
| int ret = 0; | |
| void *ehdr; | |
| rtnl_lock(); | |
| for_each_netdev_dump(net, dev, ctx->ifindex) { | |
| ehdr = ethnl_dump_put(skb, cb, | |
| ETHTOOL_MSG_TUNNEL_INFO_GET_REPLY); | |
| if (!ehdr) { | |
| ret = -EMSGSIZE; | |
| break; | |
| } | |
| ret = ethnl_fill_reply_header(skb, dev, | |
| ETHTOOL_A_TUNNEL_INFO_HEADER); | |
| if (ret < 0) { | |
| genlmsg_cancel(skb, ehdr); | |
| break; | |
| } | |
| ctx->req_info.dev = dev; | |
| ret = ethnl_tunnel_info_fill_reply(&ctx->req_info, skb); | |
| ctx->req_info.dev = NULL; | |
| if (ret < 0) { | |
| genlmsg_cancel(skb, ehdr); | |
| if (ret == -EOPNOTSUPP) | |
| continue; | |
| break; | |
| } | |
| genlmsg_end(skb, ehdr); | |
| } | |
| rtnl_unlock(); | |
| if (ret == -EMSGSIZE && skb->len) | |
| return skb->len; | |
| return ret; | |
| } |