+ return s[n + 1] == ')' ? n + 2 : -EINVAL;
+ }
+ }
+
+ {
+ uint32_t port;
+ int n;
+
+ if (ovs_scan(s, "tnl_pop(%"SCNi32")%n", &port, &n)) {
+ nl_msg_put_u32(actions, OVS_ACTION_ATTR_TUNNEL_POP, port);
+ return n;
+ }
+ }
+
+ {
+ int retval;
+
+ retval = parse_conntrack_action(s, actions);
+ if (retval) {
+ return retval;
+ }
+ }
+
+ {
+ struct ovs_action_push_tnl data;
+ int n;
+
+ n = ovs_parse_tnl_push(s, &data);
+ if (n > 0) {
+ odp_put_tnl_push_action(actions, &data);
+ return n;
+ } else if (n < 0) {
+ return n;
+ }
+ }
+ return -EINVAL;
+}
+
+/* Parses the string representation of datapath actions, in the format output
+ * by format_odp_action(). Returns 0 if successful, otherwise a positive errno
+ * value. On success, the ODP actions are appended to 'actions' as a series of
+ * Netlink attributes. On failure, no data is appended to 'actions'. Either
+ * way, 'actions''s data might be reallocated. */
+int
+odp_actions_from_string(const char *s, const struct simap *port_names,
+ struct ofpbuf *actions)
+{
+ size_t old_size;
+
+ if (!strcasecmp(s, "drop")) {
+ return 0;
+ }
+
+ old_size = actions->size;
+ for (;;) {
+ int retval;
+
+ s += strspn(s, delimiters);
+ if (!*s) {
+ return 0;
+ }
+
+ retval = parse_odp_action(s, port_names, actions);
+ if (retval < 0 || !strchr(delimiters, s[retval])) {
+ actions->size = old_size;
+ return -retval;
+ }
+ s += retval;
+ }
+
+ return 0;
+}
+\f
+static const struct attr_len_tbl ovs_vxlan_ext_attr_lens[OVS_VXLAN_EXT_MAX + 1] = {
+ [OVS_VXLAN_EXT_GBP] = { .len = 4 },
+};
+
+static const struct attr_len_tbl ovs_tun_key_attr_lens[OVS_TUNNEL_KEY_ATTR_MAX + 1] = {
+ [OVS_TUNNEL_KEY_ATTR_ID] = { .len = 8 },
+ [OVS_TUNNEL_KEY_ATTR_IPV4_SRC] = { .len = 4 },
+ [OVS_TUNNEL_KEY_ATTR_IPV4_DST] = { .len = 4 },
+ [OVS_TUNNEL_KEY_ATTR_TOS] = { .len = 1 },
+ [OVS_TUNNEL_KEY_ATTR_TTL] = { .len = 1 },
+ [OVS_TUNNEL_KEY_ATTR_DONT_FRAGMENT] = { .len = 0 },
+ [OVS_TUNNEL_KEY_ATTR_CSUM] = { .len = 0 },
+ [OVS_TUNNEL_KEY_ATTR_TP_SRC] = { .len = 2 },
+ [OVS_TUNNEL_KEY_ATTR_TP_DST] = { .len = 2 },
+ [OVS_TUNNEL_KEY_ATTR_OAM] = { .len = 0 },
+ [OVS_TUNNEL_KEY_ATTR_GENEVE_OPTS] = { .len = ATTR_LEN_VARIABLE },
+ [OVS_TUNNEL_KEY_ATTR_VXLAN_OPTS] = { .len = ATTR_LEN_NESTED,
+ .next = ovs_vxlan_ext_attr_lens ,
+ .next_max = OVS_VXLAN_EXT_MAX},
+ [OVS_TUNNEL_KEY_ATTR_IPV6_SRC] = { .len = 16 },
+ [OVS_TUNNEL_KEY_ATTR_IPV6_DST] = { .len = 16 },
+};
+
+static const struct attr_len_tbl ovs_flow_key_attr_lens[OVS_KEY_ATTR_MAX + 1] = {
+ [OVS_KEY_ATTR_ENCAP] = { .len = ATTR_LEN_NESTED },
+ [OVS_KEY_ATTR_PRIORITY] = { .len = 4 },
+ [OVS_KEY_ATTR_SKB_MARK] = { .len = 4 },
+ [OVS_KEY_ATTR_DP_HASH] = { .len = 4 },
+ [OVS_KEY_ATTR_RECIRC_ID] = { .len = 4 },
+ [OVS_KEY_ATTR_TUNNEL] = { .len = ATTR_LEN_NESTED,
+ .next = ovs_tun_key_attr_lens,
+ .next_max = OVS_TUNNEL_KEY_ATTR_MAX },
+ [OVS_KEY_ATTR_IN_PORT] = { .len = 4 },
+ [OVS_KEY_ATTR_ETHERNET] = { .len = sizeof(struct ovs_key_ethernet) },
+ [OVS_KEY_ATTR_VLAN] = { .len = 2 },
+ [OVS_KEY_ATTR_ETHERTYPE] = { .len = 2 },
+ [OVS_KEY_ATTR_MPLS] = { .len = ATTR_LEN_VARIABLE },
+ [OVS_KEY_ATTR_IPV4] = { .len = sizeof(struct ovs_key_ipv4) },
+ [OVS_KEY_ATTR_IPV6] = { .len = sizeof(struct ovs_key_ipv6) },
+ [OVS_KEY_ATTR_TCP] = { .len = sizeof(struct ovs_key_tcp) },
+ [OVS_KEY_ATTR_TCP_FLAGS] = { .len = 2 },
+ [OVS_KEY_ATTR_UDP] = { .len = sizeof(struct ovs_key_udp) },
+ [OVS_KEY_ATTR_SCTP] = { .len = sizeof(struct ovs_key_sctp) },
+ [OVS_KEY_ATTR_ICMP] = { .len = sizeof(struct ovs_key_icmp) },
+ [OVS_KEY_ATTR_ICMPV6] = { .len = sizeof(struct ovs_key_icmpv6) },
+ [OVS_KEY_ATTR_ARP] = { .len = sizeof(struct ovs_key_arp) },
+ [OVS_KEY_ATTR_ND] = { .len = sizeof(struct ovs_key_nd) },
+ [OVS_KEY_ATTR_CT_STATE] = { .len = 4 },
+ [OVS_KEY_ATTR_CT_ZONE] = { .len = 2 },
+ [OVS_KEY_ATTR_CT_MARK] = { .len = 4 },
+ [OVS_KEY_ATTR_CT_LABELS] = { .len = sizeof(struct ovs_key_ct_labels) },
+};
+
+/* Returns the correct length of the payload for a flow key attribute of the
+ * specified 'type', ATTR_LEN_INVALID if 'type' is unknown, ATTR_LEN_VARIABLE
+ * if the attribute's payload is variable length, or ATTR_LEN_NESTED if the
+ * payload is a nested type. */
+static int
+odp_key_attr_len(const struct attr_len_tbl tbl[], int max_len, uint16_t type)
+{
+ if (type > max_len) {
+ return ATTR_LEN_INVALID;
+ }
+
+ return tbl[type].len;
+}
+
+static void
+format_generic_odp_key(const struct nlattr *a, struct ds *ds)
+{
+ size_t len = nl_attr_get_size(a);
+ if (len) {
+ const uint8_t *unspec;
+ unsigned int i;
+
+ unspec = nl_attr_get(a);
+ for (i = 0; i < len; i++) {
+ if (i) {
+ ds_put_char(ds, ' ');
+ }
+ ds_put_format(ds, "%02x", unspec[i]);
+ }
+ }
+}
+
+static const char *
+ovs_frag_type_to_string(enum ovs_frag_type type)
+{
+ switch (type) {
+ case OVS_FRAG_TYPE_NONE:
+ return "no";
+ case OVS_FRAG_TYPE_FIRST:
+ return "first";
+ case OVS_FRAG_TYPE_LATER:
+ return "later";
+ case __OVS_FRAG_TYPE_MAX:
+ default:
+ return "<error>";
+ }
+}
+
+static enum odp_key_fitness
+odp_tun_key_from_attr__(const struct nlattr *attr,
+ const struct nlattr *flow_attrs, size_t flow_attr_len,
+ const struct flow_tnl *src_tun, struct flow_tnl *tun,
+ bool udpif)
+{
+ unsigned int left;
+ const struct nlattr *a;
+ bool ttl = false;
+ bool unknown = false;
+
+ NL_NESTED_FOR_EACH(a, left, attr) {
+ uint16_t type = nl_attr_type(a);
+ size_t len = nl_attr_get_size(a);
+ int expected_len = odp_key_attr_len(ovs_tun_key_attr_lens,
+ OVS_TUNNEL_ATTR_MAX, type);
+
+ if (len != expected_len && expected_len >= 0) {
+ return ODP_FIT_ERROR;
+ }
+
+ switch (type) {
+ case OVS_TUNNEL_KEY_ATTR_ID:
+ tun->tun_id = nl_attr_get_be64(a);
+ tun->flags |= FLOW_TNL_F_KEY;
+ break;
+ case OVS_TUNNEL_KEY_ATTR_IPV4_SRC:
+ tun->ip_src = nl_attr_get_be32(a);
+ break;
+ case OVS_TUNNEL_KEY_ATTR_IPV4_DST:
+ tun->ip_dst = nl_attr_get_be32(a);
+ break;
+ case OVS_TUNNEL_KEY_ATTR_IPV6_SRC:
+ tun->ipv6_src = nl_attr_get_in6_addr(a);
+ break;
+ case OVS_TUNNEL_KEY_ATTR_IPV6_DST:
+ tun->ipv6_dst = nl_attr_get_in6_addr(a);
+ break;
+ case OVS_TUNNEL_KEY_ATTR_TOS:
+ tun->ip_tos = nl_attr_get_u8(a);
+ break;
+ case OVS_TUNNEL_KEY_ATTR_TTL:
+ tun->ip_ttl = nl_attr_get_u8(a);
+ ttl = true;
+ break;
+ case OVS_TUNNEL_KEY_ATTR_DONT_FRAGMENT:
+ tun->flags |= FLOW_TNL_F_DONT_FRAGMENT;
+ break;
+ case OVS_TUNNEL_KEY_ATTR_CSUM:
+ tun->flags |= FLOW_TNL_F_CSUM;
+ break;
+ case OVS_TUNNEL_KEY_ATTR_TP_SRC:
+ tun->tp_src = nl_attr_get_be16(a);
+ break;
+ case OVS_TUNNEL_KEY_ATTR_TP_DST:
+ tun->tp_dst = nl_attr_get_be16(a);
+ break;
+ case OVS_TUNNEL_KEY_ATTR_OAM:
+ tun->flags |= FLOW_TNL_F_OAM;
+ break;
+ case OVS_TUNNEL_KEY_ATTR_VXLAN_OPTS: {
+ static const struct nl_policy vxlan_opts_policy[] = {
+ [OVS_VXLAN_EXT_GBP] = { .type = NL_A_U32 },
+ };
+ struct nlattr *ext[ARRAY_SIZE(vxlan_opts_policy)];
+
+ if (!nl_parse_nested(a, vxlan_opts_policy, ext, ARRAY_SIZE(ext))) {
+ return ODP_FIT_ERROR;
+ }
+
+ if (ext[OVS_VXLAN_EXT_GBP]) {
+ uint32_t gbp = nl_attr_get_u32(ext[OVS_VXLAN_EXT_GBP]);
+
+ tun->gbp_id = htons(gbp & 0xFFFF);
+ tun->gbp_flags = (gbp >> 16) & 0xFF;
+ }
+
+ break;
+ }
+ case OVS_TUNNEL_KEY_ATTR_GENEVE_OPTS:
+ if (tun_metadata_from_geneve_nlattr(a, flow_attrs, flow_attr_len,
+ src_tun, udpif, tun)) {
+ return ODP_FIT_ERROR;
+ }
+ break;
+
+ default:
+ /* Allow this to show up as unexpected, if there are unknown
+ * tunnel attribute, eventually resulting in ODP_FIT_TOO_MUCH. */
+ unknown = true;
+ break;
+ }
+ }
+
+ if (!ttl) {
+ return ODP_FIT_ERROR;
+ }
+ if (unknown) {
+ return ODP_FIT_TOO_MUCH;
+ }
+ return ODP_FIT_PERFECT;
+}
+
+enum odp_key_fitness
+odp_tun_key_from_attr(const struct nlattr *attr, bool udpif,
+ struct flow_tnl *tun)
+{
+ memset(tun, 0, sizeof *tun);
+ return odp_tun_key_from_attr__(attr, NULL, 0, NULL, tun, udpif);
+}
+
+static void
+tun_key_to_attr(struct ofpbuf *a, const struct flow_tnl *tun_key,
+ const struct flow_tnl *tun_flow_key,
+ const struct ofpbuf *key_buf)
+{
+ size_t tun_key_ofs;
+
+ tun_key_ofs = nl_msg_start_nested(a, OVS_KEY_ATTR_TUNNEL);
+
+ /* tun_id != 0 without FLOW_TNL_F_KEY is valid if tun_key is a mask. */
+ if (tun_key->tun_id || tun_key->flags & FLOW_TNL_F_KEY) {
+ nl_msg_put_be64(a, OVS_TUNNEL_KEY_ATTR_ID, tun_key->tun_id);
+ }
+ if (tun_key->ip_src) {
+ nl_msg_put_be32(a, OVS_TUNNEL_KEY_ATTR_IPV4_SRC, tun_key->ip_src);
+ }
+ if (tun_key->ip_dst) {
+ nl_msg_put_be32(a, OVS_TUNNEL_KEY_ATTR_IPV4_DST, tun_key->ip_dst);
+ }
+ if (ipv6_addr_is_set(&tun_key->ipv6_src)) {
+ nl_msg_put_in6_addr(a, OVS_TUNNEL_KEY_ATTR_IPV6_SRC, &tun_key->ipv6_src);
+ }
+ if (ipv6_addr_is_set(&tun_key->ipv6_dst)) {
+ nl_msg_put_in6_addr(a, OVS_TUNNEL_KEY_ATTR_IPV6_DST, &tun_key->ipv6_dst);
+ }
+ if (tun_key->ip_tos) {
+ nl_msg_put_u8(a, OVS_TUNNEL_KEY_ATTR_TOS, tun_key->ip_tos);
+ }
+ nl_msg_put_u8(a, OVS_TUNNEL_KEY_ATTR_TTL, tun_key->ip_ttl);
+ if (tun_key->flags & FLOW_TNL_F_DONT_FRAGMENT) {
+ nl_msg_put_flag(a, OVS_TUNNEL_KEY_ATTR_DONT_FRAGMENT);
+ }
+ if (tun_key->flags & FLOW_TNL_F_CSUM) {
+ nl_msg_put_flag(a, OVS_TUNNEL_KEY_ATTR_CSUM);
+ }
+ if (tun_key->tp_src) {
+ nl_msg_put_be16(a, OVS_TUNNEL_KEY_ATTR_TP_SRC, tun_key->tp_src);
+ }
+ if (tun_key->tp_dst) {
+ nl_msg_put_be16(a, OVS_TUNNEL_KEY_ATTR_TP_DST, tun_key->tp_dst);
+ }
+ if (tun_key->flags & FLOW_TNL_F_OAM) {
+ nl_msg_put_flag(a, OVS_TUNNEL_KEY_ATTR_OAM);
+ }
+ if (tun_key->gbp_flags || tun_key->gbp_id) {
+ size_t vxlan_opts_ofs;
+
+ vxlan_opts_ofs = nl_msg_start_nested(a, OVS_TUNNEL_KEY_ATTR_VXLAN_OPTS);
+ nl_msg_put_u32(a, OVS_VXLAN_EXT_GBP,
+ (tun_key->gbp_flags << 16) | ntohs(tun_key->gbp_id));
+ nl_msg_end_nested(a, vxlan_opts_ofs);
+ }
+ tun_metadata_to_geneve_nlattr(tun_key, tun_flow_key, key_buf, a);
+
+ nl_msg_end_nested(a, tun_key_ofs);
+}
+
+static bool
+odp_mask_attr_is_wildcard(const struct nlattr *ma)
+{
+ return is_all_zeros(nl_attr_get(ma), nl_attr_get_size(ma));
+}
+
+static bool
+odp_mask_is_exact(enum ovs_key_attr attr, const void *mask, size_t size)
+{
+ if (attr == OVS_KEY_ATTR_TCP_FLAGS) {
+ return TCP_FLAGS(*(ovs_be16 *)mask) == TCP_FLAGS(OVS_BE16_MAX);
+ }
+ if (attr == OVS_KEY_ATTR_IPV6) {
+ const struct ovs_key_ipv6 *ipv6_mask = mask;
+
+ return
+ ((ipv6_mask->ipv6_label & htonl(IPV6_LABEL_MASK))
+ == htonl(IPV6_LABEL_MASK))
+ && ipv6_mask->ipv6_proto == UINT8_MAX
+ && ipv6_mask->ipv6_tclass == UINT8_MAX
+ && ipv6_mask->ipv6_hlimit == UINT8_MAX
+ && ipv6_mask->ipv6_frag == UINT8_MAX
+ && ipv6_mask_is_exact((const struct in6_addr *)ipv6_mask->ipv6_src)
+ && ipv6_mask_is_exact((const struct in6_addr *)ipv6_mask->ipv6_dst);
+ }
+ if (attr == OVS_KEY_ATTR_TUNNEL) {
+ return false;
+ }
+
+ if (attr == OVS_KEY_ATTR_ARP) {
+ /* ARP key has padding, ignore it. */
+ BUILD_ASSERT_DECL(sizeof(struct ovs_key_arp) == 24);
+ BUILD_ASSERT_DECL(offsetof(struct ovs_key_arp, arp_tha) == 10 + 6);
+ size = offsetof(struct ovs_key_arp, arp_tha) + ETH_ADDR_LEN;
+ ovs_assert(((uint16_t *)mask)[size/2] == 0);
+ }
+
+ return is_all_ones(mask, size);
+}
+
+static bool
+odp_mask_attr_is_exact(const struct nlattr *ma)
+{
+ enum ovs_key_attr attr = nl_attr_type(ma);
+ const void *mask;
+ size_t size;
+
+ if (attr == OVS_KEY_ATTR_TUNNEL) {
+ return false;
+ } else {
+ mask = nl_attr_get(ma);
+ size = nl_attr_get_size(ma);
+ }
+
+ return odp_mask_is_exact(attr, mask, size);
+}
+
+void
+odp_portno_names_set(struct hmap *portno_names, odp_port_t port_no,
+ char *port_name)
+{
+ struct odp_portno_names *odp_portno_names;
+
+ odp_portno_names = xmalloc(sizeof *odp_portno_names);
+ odp_portno_names->port_no = port_no;
+ odp_portno_names->name = xstrdup(port_name);
+ hmap_insert(portno_names, &odp_portno_names->hmap_node,
+ hash_odp_port(port_no));
+}
+
+static char *
+odp_portno_names_get(const struct hmap *portno_names, odp_port_t port_no)
+{
+ struct odp_portno_names *odp_portno_names;
+
+ HMAP_FOR_EACH_IN_BUCKET (odp_portno_names, hmap_node,
+ hash_odp_port(port_no), portno_names) {
+ if (odp_portno_names->port_no == port_no) {
+ return odp_portno_names->name;
+ }
+ }
+ return NULL;
+}
+
+void
+odp_portno_names_destroy(struct hmap *portno_names)
+{
+ struct odp_portno_names *odp_portno_names, *odp_portno_names_next;
+ HMAP_FOR_EACH_SAFE (odp_portno_names, odp_portno_names_next,
+ hmap_node, portno_names) {
+ hmap_remove(portno_names, &odp_portno_names->hmap_node);
+ free(odp_portno_names->name);
+ free(odp_portno_names);
+ }
+}
+
+/* Format helpers. */
+
+static void
+format_eth(struct ds *ds, const char *name, const struct eth_addr key,
+ const struct eth_addr *mask, bool verbose)
+{
+ bool mask_empty = mask && eth_addr_is_zero(*mask);
+
+ if (verbose || !mask_empty) {
+ bool mask_full = !mask || eth_mask_is_exact(*mask);
+
+ if (mask_full) {
+ ds_put_format(ds, "%s="ETH_ADDR_FMT",", name, ETH_ADDR_ARGS(key));
+ } else {
+ ds_put_format(ds, "%s=", name);
+ eth_format_masked(key, mask, ds);
+ ds_put_char(ds, ',');
+ }
+ }
+}
+
+static void
+format_be64(struct ds *ds, const char *name, ovs_be64 key,
+ const ovs_be64 *mask, bool verbose)
+{
+ bool mask_empty = mask && !*mask;
+
+ if (verbose || !mask_empty) {
+ bool mask_full = !mask || *mask == OVS_BE64_MAX;
+
+ ds_put_format(ds, "%s=0x%"PRIx64, name, ntohll(key));
+ if (!mask_full) { /* Partially masked. */
+ ds_put_format(ds, "/%#"PRIx64, ntohll(*mask));
+ }
+ ds_put_char(ds, ',');
+ }
+}
+
+static void
+format_ipv4(struct ds *ds, const char *name, ovs_be32 key,
+ const ovs_be32 *mask, bool verbose)
+{
+ bool mask_empty = mask && !*mask;
+
+ if (verbose || !mask_empty) {
+ bool mask_full = !mask || *mask == OVS_BE32_MAX;
+
+ ds_put_format(ds, "%s="IP_FMT, name, IP_ARGS(key));
+ if (!mask_full) { /* Partially masked. */
+ ds_put_format(ds, "/"IP_FMT, IP_ARGS(*mask));
+ }
+ ds_put_char(ds, ',');
+ }
+}
+
+static void
+format_in6_addr(struct ds *ds, const char *name,
+ const struct in6_addr *key,
+ const struct in6_addr *mask,
+ bool verbose)
+{
+ char buf[INET6_ADDRSTRLEN];
+ bool mask_empty = mask && ipv6_mask_is_any(mask);
+
+ if (verbose || !mask_empty) {
+ bool mask_full = !mask || ipv6_mask_is_exact(mask);
+
+ inet_ntop(AF_INET6, key, buf, sizeof buf);
+ ds_put_format(ds, "%s=%s", name, buf);
+ if (!mask_full) { /* Partially masked. */
+ inet_ntop(AF_INET6, mask, buf, sizeof buf);
+ ds_put_format(ds, "/%s", buf);
+ }
+ ds_put_char(ds, ',');
+ }
+}
+
+static void
+format_ipv6(struct ds *ds, const char *name, const ovs_be32 key_[4],
+ const ovs_be32 (*mask_)[4], bool verbose)
+{
+ format_in6_addr(ds, name,
+ (const struct in6_addr *)key_,
+ mask_ ? (const struct in6_addr *)*mask_ : NULL,
+ verbose);
+}
+
+static void
+format_ipv6_label(struct ds *ds, const char *name, ovs_be32 key,
+ const ovs_be32 *mask, bool verbose)
+{
+ bool mask_empty = mask && !*mask;
+
+ if (verbose || !mask_empty) {
+ bool mask_full = !mask
+ || (*mask & htonl(IPV6_LABEL_MASK)) == htonl(IPV6_LABEL_MASK);
+
+ ds_put_format(ds, "%s=%#"PRIx32, name, ntohl(key));
+ if (!mask_full) { /* Partially masked. */
+ ds_put_format(ds, "/%#"PRIx32, ntohl(*mask));
+ }
+ ds_put_char(ds, ',');
+ }
+}
+
+static void
+format_u8x(struct ds *ds, const char *name, uint8_t key,
+ const uint8_t *mask, bool verbose)
+{
+ bool mask_empty = mask && !*mask;
+
+ if (verbose || !mask_empty) {
+ bool mask_full = !mask || *mask == UINT8_MAX;
+
+ ds_put_format(ds, "%s=%#"PRIx8, name, key);
+ if (!mask_full) { /* Partially masked. */
+ ds_put_format(ds, "/%#"PRIx8, *mask);
+ }
+ ds_put_char(ds, ',');
+ }
+}
+
+static void
+format_u8u(struct ds *ds, const char *name, uint8_t key,
+ const uint8_t *mask, bool verbose)
+{
+ bool mask_empty = mask && !*mask;
+
+ if (verbose || !mask_empty) {
+ bool mask_full = !mask || *mask == UINT8_MAX;
+
+ ds_put_format(ds, "%s=%"PRIu8, name, key);
+ if (!mask_full) { /* Partially masked. */
+ ds_put_format(ds, "/%#"PRIx8, *mask);
+ }
+ ds_put_char(ds, ',');
+ }
+}
+
+static void
+format_be16(struct ds *ds, const char *name, ovs_be16 key,
+ const ovs_be16 *mask, bool verbose)
+{
+ bool mask_empty = mask && !*mask;
+
+ if (verbose || !mask_empty) {
+ bool mask_full = !mask || *mask == OVS_BE16_MAX;
+
+ ds_put_format(ds, "%s=%"PRIu16, name, ntohs(key));
+ if (!mask_full) { /* Partially masked. */
+ ds_put_format(ds, "/%#"PRIx16, ntohs(*mask));