Author | Tokens | Token Proportion | Commits | Commit Proportion |
---|---|---|---|---|
Steen Hegelund | 5003 | 71.79% | 35 | 68.63% |
Daniel Machon | 1534 | 22.01% | 7 | 13.73% |
Asbjörn Sloth Tönnesen | 223 | 3.20% | 5 | 9.80% |
Horatiu Vultur | 207 | 2.97% | 3 | 5.88% |
Ratheesh Kannoth | 2 | 0.03% | 1 | 1.96% |
Total | 6969 | 51 |
// SPDX-License-Identifier: GPL-2.0+ /* Microchip VCAP API * * Copyright (c) 2022 Microchip Technology Inc. and its subsidiaries. */ #include <net/tc_act/tc_gate.h> #include <net/tcp.h> #include "sparx5_tc.h" #include "vcap_api.h" #include "vcap_api_client.h" #include "vcap_tc.h" #include "sparx5_main.h" #include "sparx5_vcap_impl.h" #define SPX5_MAX_RULE_SIZE 13 /* allows X1, X2, X4, X6 and X12 rules */ /* Collect keysets and type ids for multiple rules per size */ struct sparx5_wildcard_rule { bool selected; u8 value; u8 mask; enum vcap_keyfield_set keyset; }; struct sparx5_multiple_rules { struct sparx5_wildcard_rule rule[SPX5_MAX_RULE_SIZE]; }; struct sparx5_tc_flower_template { struct list_head list; /* for insertion in the list of templates */ int cid; /* chain id */ enum vcap_keyfield_set orig; /* keyset used before the template */ enum vcap_keyfield_set keyset; /* new keyset used by template */ u16 l3_proto; /* protocol specified in the template */ }; /* SparX-5 VCAP fragment types: * 0 = no fragment, 1 = initial fragment, * 2 = suspicious fragment, 3 = valid follow-up fragment */ enum { /* key / mask */ FRAG_NOT = 0x03, /* 0 / 3 */ FRAG_SOME = 0x11, /* 1 / 1 */ FRAG_FIRST = 0x13, /* 1 / 3 */ FRAG_LATER = 0x33, /* 3 / 3 */ FRAG_INVAL = 0xff, /* invalid */ }; /* Flower fragment flag to VCAP fragment type mapping */ static const u8 sparx5_vcap_frag_map[4][4] = { /* is_frag */ { FRAG_INVAL, FRAG_INVAL, FRAG_INVAL, FRAG_FIRST }, /* 0/0 */ { FRAG_NOT, FRAG_NOT, FRAG_INVAL, FRAG_INVAL }, /* 0/1 */ { FRAG_INVAL, FRAG_INVAL, FRAG_INVAL, FRAG_INVAL }, /* 1/0 */ { FRAG_SOME, FRAG_LATER, FRAG_INVAL, FRAG_FIRST } /* 1/1 */ /* 0/0 0/1 1/0 1/1 <-- first_frag */ }; static int sparx5_tc_flower_es0_tpid(struct vcap_tc_flower_parse_usage *st) { int err = 0; switch (st->tpid) { case ETH_P_8021Q: err = vcap_rule_add_key_u32(st->vrule, VCAP_KF_8021Q_TPID, SPX5_TPID_SEL_8100, ~0); break; case ETH_P_8021AD: err = vcap_rule_add_key_u32(st->vrule, VCAP_KF_8021Q_TPID, SPX5_TPID_SEL_88A8, ~0); break; default: NL_SET_ERR_MSG_MOD(st->fco->common.extack, "Invalid vlan proto"); err = -EINVAL; break; } return err; } static int sparx5_tc_flower_handler_basic_usage(struct vcap_tc_flower_parse_usage *st) { struct flow_match_basic mt; int err = 0; flow_rule_match_basic(st->frule, &mt); if (mt.mask->n_proto) { st->l3_proto = be16_to_cpu(mt.key->n_proto); if (!sparx5_vcap_is_known_etype(st->admin, st->l3_proto)) { err = vcap_rule_add_key_u32(st->vrule, VCAP_KF_ETYPE, st->l3_proto, ~0); if (err) goto out; } else if (st->l3_proto == ETH_P_IP) { err = vcap_rule_add_key_bit(st->vrule, VCAP_KF_IP4_IS, VCAP_BIT_1); if (err) goto out; } else if (st->l3_proto == ETH_P_IPV6) { err = vcap_rule_add_key_bit(st->vrule, VCAP_KF_IP4_IS, VCAP_BIT_0); if (err) goto out; if (st->admin->vtype == VCAP_TYPE_IS0) { err = vcap_rule_add_key_bit(st->vrule, VCAP_KF_IP_SNAP_IS, VCAP_BIT_1); if (err) goto out; } } } if (mt.mask->ip_proto) { st->l4_proto = mt.key->ip_proto; if (st->l4_proto == IPPROTO_TCP) { err = vcap_rule_add_key_bit(st->vrule, VCAP_KF_TCP_IS, VCAP_BIT_1); if (err) goto out; } else if (st->l4_proto == IPPROTO_UDP) { err = vcap_rule_add_key_bit(st->vrule, VCAP_KF_TCP_IS, VCAP_BIT_0); if (err) goto out; if (st->admin->vtype == VCAP_TYPE_IS0) { err = vcap_rule_add_key_bit(st->vrule, VCAP_KF_TCP_UDP_IS, VCAP_BIT_1); if (err) goto out; } } else { err = vcap_rule_add_key_u32(st->vrule, VCAP_KF_L3_IP_PROTO, st->l4_proto, ~0); if (err) goto out; } } st->used_keys |= BIT_ULL(FLOW_DISSECTOR_KEY_BASIC); return err; out: NL_SET_ERR_MSG_MOD(st->fco->common.extack, "ip_proto parse error"); return err; } static int sparx5_tc_flower_handler_control_usage(struct vcap_tc_flower_parse_usage *st) { struct netlink_ext_ack *extack = st->fco->common.extack; struct flow_match_control mt; u32 value, mask; int err = 0; flow_rule_match_control(st->frule, &mt); if (mt.mask->flags & (FLOW_DIS_IS_FRAGMENT | FLOW_DIS_FIRST_FRAG)) { u8 is_frag_key = !!(mt.key->flags & FLOW_DIS_IS_FRAGMENT); u8 is_frag_mask = !!(mt.mask->flags & FLOW_DIS_IS_FRAGMENT); u8 is_frag_idx = (is_frag_key << 1) | is_frag_mask; u8 first_frag_key = !!(mt.key->flags & FLOW_DIS_FIRST_FRAG); u8 first_frag_mask = !!(mt.mask->flags & FLOW_DIS_FIRST_FRAG); u8 first_frag_idx = (first_frag_key << 1) | first_frag_mask; /* Lookup verdict based on the 2 + 2 input bits */ u8 vdt = sparx5_vcap_frag_map[is_frag_idx][first_frag_idx]; if (vdt == FRAG_INVAL) { NL_SET_ERR_MSG_MOD(extack, "Match on invalid fragment flag combination"); return -EINVAL; } /* Extract VCAP fragment key and mask from verdict */ value = (vdt >> 4) & 0x3; mask = vdt & 0x3; err = vcap_rule_add_key_u32(st->vrule, VCAP_KF_L3_FRAGMENT_TYPE, value, mask); if (err) { NL_SET_ERR_MSG_MOD(extack, "ip_frag parse error"); return err; } } if (!flow_rule_is_supp_control_flags(FLOW_DIS_IS_FRAGMENT | FLOW_DIS_FIRST_FRAG, mt.mask->flags, extack)) return -EOPNOTSUPP; st->used_keys |= BIT_ULL(FLOW_DISSECTOR_KEY_CONTROL); return err; } static int sparx5_tc_flower_handler_cvlan_usage(struct vcap_tc_flower_parse_usage *st) { if (st->admin->vtype != VCAP_TYPE_IS0) { NL_SET_ERR_MSG_MOD(st->fco->common.extack, "cvlan not supported in this VCAP"); return -EINVAL; } return vcap_tc_flower_handler_cvlan_usage(st); } static int sparx5_tc_flower_handler_vlan_usage(struct vcap_tc_flower_parse_usage *st) { enum vcap_key_field vid_key = VCAP_KF_8021Q_VID_CLS; enum vcap_key_field pcp_key = VCAP_KF_8021Q_PCP_CLS; int err; if (st->admin->vtype == VCAP_TYPE_IS0) { vid_key = VCAP_KF_8021Q_VID0; pcp_key = VCAP_KF_8021Q_PCP0; } err = vcap_tc_flower_handler_vlan_usage(st, vid_key, pcp_key); if (err) return err; if (st->admin->vtype == VCAP_TYPE_ES0 && st->tpid) err = sparx5_tc_flower_es0_tpid(st); return err; } static int (*sparx5_tc_flower_usage_handlers[])(struct vcap_tc_flower_parse_usage *st) = { [FLOW_DISSECTOR_KEY_ETH_ADDRS] = vcap_tc_flower_handler_ethaddr_usage, [FLOW_DISSECTOR_KEY_IPV4_ADDRS] = vcap_tc_flower_handler_ipv4_usage, [FLOW_DISSECTOR_KEY_IPV6_ADDRS] = vcap_tc_flower_handler_ipv6_usage, [FLOW_DISSECTOR_KEY_CONTROL] = sparx5_tc_flower_handler_control_usage, [FLOW_DISSECTOR_KEY_PORTS] = vcap_tc_flower_handler_portnum_usage, [FLOW_DISSECTOR_KEY_BASIC] = sparx5_tc_flower_handler_basic_usage, [FLOW_DISSECTOR_KEY_CVLAN] = sparx5_tc_flower_handler_cvlan_usage, [FLOW_DISSECTOR_KEY_VLAN] = sparx5_tc_flower_handler_vlan_usage, [FLOW_DISSECTOR_KEY_TCP] = vcap_tc_flower_handler_tcp_usage, [FLOW_DISSECTOR_KEY_ARP] = vcap_tc_flower_handler_arp_usage, [FLOW_DISSECTOR_KEY_IP] = vcap_tc_flower_handler_ip_usage, }; static int sparx5_tc_use_dissectors(struct vcap_tc_flower_parse_usage *st, struct vcap_admin *admin, struct vcap_rule *vrule) { int idx, err = 0; for (idx = 0; idx < ARRAY_SIZE(sparx5_tc_flower_usage_handlers); ++idx) { if (!flow_rule_match_key(st->frule, idx)) continue; if (!sparx5_tc_flower_usage_handlers[idx]) continue; err = sparx5_tc_flower_usage_handlers[idx](st); if (err) return err; } if (st->frule->match.dissector->used_keys ^ st->used_keys) { NL_SET_ERR_MSG_MOD(st->fco->common.extack, "Unsupported match item"); return -ENOENT; } return err; } static int sparx5_tc_flower_action_check(struct vcap_control *vctrl, struct net_device *ndev, struct flow_cls_offload *fco, bool ingress) { struct flow_rule *rule = flow_cls_offload_flow_rule(fco); struct flow_action_entry *actent, *last_actent = NULL; struct flow_action *act = &rule->action; u64 action_mask = 0; int idx; if (!flow_action_has_entries(act)) { NL_SET_ERR_MSG_MOD(fco->common.extack, "No actions"); return -EINVAL; } if (!flow_action_basic_hw_stats_check(act, fco->common.extack)) return -EOPNOTSUPP; flow_action_for_each(idx, actent, act) { if (action_mask & BIT(actent->id)) { NL_SET_ERR_MSG_MOD(fco->common.extack, "More actions of the same type"); return -EINVAL; } action_mask |= BIT(actent->id); last_actent = actent; /* Save last action for later check */ } /* Check if last action is a goto * The last chain/lookup does not need to have a goto action */ if (last_actent->id == FLOW_ACTION_GOTO) { /* Check if the destination chain is in one of the VCAPs */ if (!vcap_is_next_lookup(vctrl, fco->common.chain_index, last_actent->chain_index)) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Invalid goto chain"); return -EINVAL; } } else if (!vcap_is_last_chain(vctrl, fco->common.chain_index, ingress)) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Last action must be 'goto'"); return -EINVAL; } /* Catch unsupported combinations of actions */ if (action_mask & BIT(FLOW_ACTION_TRAP) && action_mask & BIT(FLOW_ACTION_ACCEPT)) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Cannot combine pass and trap action"); return -EOPNOTSUPP; } if (action_mask & BIT(FLOW_ACTION_VLAN_PUSH) && action_mask & BIT(FLOW_ACTION_VLAN_POP)) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Cannot combine vlan push and pop action"); return -EOPNOTSUPP; } if (action_mask & BIT(FLOW_ACTION_VLAN_PUSH) && action_mask & BIT(FLOW_ACTION_VLAN_MANGLE)) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Cannot combine vlan push and modify action"); return -EOPNOTSUPP; } if (action_mask & BIT(FLOW_ACTION_VLAN_POP) && action_mask & BIT(FLOW_ACTION_VLAN_MANGLE)) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Cannot combine vlan pop and modify action"); return -EOPNOTSUPP; } return 0; } /* Add a rule counter action */ static int sparx5_tc_add_rule_counter(struct vcap_admin *admin, struct vcap_rule *vrule) { int err; switch (admin->vtype) { case VCAP_TYPE_IS0: break; case VCAP_TYPE_ES0: err = vcap_rule_mod_action_u32(vrule, VCAP_AF_ESDX, vrule->id); if (err) return err; vcap_rule_set_counter_id(vrule, vrule->id); break; case VCAP_TYPE_IS2: case VCAP_TYPE_ES2: err = vcap_rule_mod_action_u32(vrule, VCAP_AF_CNT_ID, vrule->id); if (err) return err; vcap_rule_set_counter_id(vrule, vrule->id); break; default: pr_err("%s:%d: vcap type: %d not supported\n", __func__, __LINE__, admin->vtype); break; } return 0; } /* Collect all port keysets and apply the first of them, possibly wildcarded */ static int sparx5_tc_select_protocol_keyset(struct net_device *ndev, struct vcap_rule *vrule, struct vcap_admin *admin, u16 l3_proto, struct sparx5_multiple_rules *multi) { struct sparx5_port *port = netdev_priv(ndev); struct vcap_keyset_list portkeysetlist = {}; enum vcap_keyfield_set portkeysets[10] = {}; struct vcap_keyset_list matches = {}; enum vcap_keyfield_set keysets[10]; int idx, jdx, err = 0, count = 0; struct sparx5_wildcard_rule *mru; const struct vcap_set *kinfo; struct vcap_control *vctrl; vctrl = port->sparx5->vcap_ctrl; /* Find the keysets that the rule can use */ matches.keysets = keysets; matches.max = ARRAY_SIZE(keysets); if (!vcap_rule_find_keysets(vrule, &matches)) return -EINVAL; /* Find the keysets that the port configuration supports */ portkeysetlist.max = ARRAY_SIZE(portkeysets); portkeysetlist.keysets = portkeysets; err = sparx5_vcap_get_port_keyset(ndev, admin, vrule->vcap_chain_id, l3_proto, &portkeysetlist); if (err) return err; /* Find the intersection of the two sets of keyset */ for (idx = 0; idx < portkeysetlist.cnt; ++idx) { kinfo = vcap_keyfieldset(vctrl, admin->vtype, portkeysetlist.keysets[idx]); if (!kinfo) continue; /* Find a port keyset that matches the required keys * If there are multiple keysets then compose a type id mask */ for (jdx = 0; jdx < matches.cnt; ++jdx) { if (portkeysetlist.keysets[idx] != matches.keysets[jdx]) continue; mru = &multi->rule[kinfo->sw_per_item]; if (!mru->selected) { mru->selected = true; mru->keyset = portkeysetlist.keysets[idx]; mru->value = kinfo->type_id; } mru->value &= kinfo->type_id; mru->mask |= kinfo->type_id; ++count; } } if (count == 0) return -EPROTO; if (l3_proto == ETH_P_ALL && count < portkeysetlist.cnt) return -ENOENT; for (idx = 0; idx < SPX5_MAX_RULE_SIZE; ++idx) { mru = &multi->rule[idx]; if (!mru->selected) continue; /* Align the mask to the combined value */ mru->mask ^= mru->value; } /* Set the chosen keyset on the rule and set a wildcarded type if there * are more than one keyset */ for (idx = 0; idx < SPX5_MAX_RULE_SIZE; ++idx) { mru = &multi->rule[idx]; if (!mru->selected) continue; vcap_set_rule_set_keyset(vrule, mru->keyset); if (count > 1) /* Some keysets do not have a type field */ vcap_rule_mod_key_u32(vrule, VCAP_KF_TYPE, mru->value, ~mru->mask); mru->selected = false; /* mark as done */ break; /* Stop here and add more rules later */ } return err; } static int sparx5_tc_add_rule_copy(struct vcap_control *vctrl, struct flow_cls_offload *fco, struct vcap_rule *erule, struct vcap_admin *admin, struct sparx5_wildcard_rule *rule) { enum vcap_key_field keylist[] = { VCAP_KF_IF_IGR_PORT_MASK, VCAP_KF_IF_IGR_PORT_MASK_SEL, VCAP_KF_IF_IGR_PORT_MASK_RNG, VCAP_KF_LOOKUP_FIRST_IS, VCAP_KF_TYPE, }; struct vcap_rule *vrule; int err; /* Add an extra rule with a special user and the new keyset */ erule->user = VCAP_USER_TC_EXTRA; vrule = vcap_copy_rule(erule); if (IS_ERR(vrule)) return PTR_ERR(vrule); /* Link the new rule to the existing rule with the cookie */ vrule->cookie = erule->cookie; vcap_filter_rule_keys(vrule, keylist, ARRAY_SIZE(keylist), true); err = vcap_set_rule_set_keyset(vrule, rule->keyset); if (err) { pr_err("%s:%d: could not set keyset %s in rule: %u\n", __func__, __LINE__, vcap_keyset_name(vctrl, rule->keyset), vrule->id); goto out; } /* Some keysets do not have a type field, so ignore return value */ vcap_rule_mod_key_u32(vrule, VCAP_KF_TYPE, rule->value, ~rule->mask); err = vcap_set_rule_set_actionset(vrule, erule->actionset); if (err) goto out; err = sparx5_tc_add_rule_counter(admin, vrule); if (err) goto out; err = vcap_val_rule(vrule, ETH_P_ALL); if (err) { pr_err("%s:%d: could not validate rule: %u\n", __func__, __LINE__, vrule->id); vcap_set_tc_exterr(fco, vrule); goto out; } err = vcap_add_rule(vrule); if (err) { pr_err("%s:%d: could not add rule: %u\n", __func__, __LINE__, vrule->id); goto out; } out: vcap_free_rule(vrule); return err; } static int sparx5_tc_add_remaining_rules(struct vcap_control *vctrl, struct flow_cls_offload *fco, struct vcap_rule *erule, struct vcap_admin *admin, struct sparx5_multiple_rules *multi) { int idx, err = 0; for (idx = 0; idx < SPX5_MAX_RULE_SIZE; ++idx) { if (!multi->rule[idx].selected) continue; err = sparx5_tc_add_rule_copy(vctrl, fco, erule, admin, &multi->rule[idx]); if (err) break; } return err; } /* Add the actionset that is the default for the VCAP type */ static int sparx5_tc_set_actionset(struct vcap_admin *admin, struct vcap_rule *vrule) { enum vcap_actionfield_set aset; int err = 0; switch (admin->vtype) { case VCAP_TYPE_IS0: aset = VCAP_AFS_CLASSIFICATION; break; case VCAP_TYPE_IS2: aset = VCAP_AFS_BASE_TYPE; break; case VCAP_TYPE_ES0: aset = VCAP_AFS_ES0; break; case VCAP_TYPE_ES2: aset = VCAP_AFS_BASE_TYPE; break; default: pr_err("%s:%d: %s\n", __func__, __LINE__, "Invalid VCAP type"); return -EINVAL; } /* Do not overwrite any current actionset */ if (vrule->actionset == VCAP_AFS_NO_VALUE) err = vcap_set_rule_set_actionset(vrule, aset); return err; } /* Add the VCAP key to match on for a rule target value */ static int sparx5_tc_add_rule_link_target(struct vcap_admin *admin, struct vcap_rule *vrule, int target_cid) { int link_val = target_cid % VCAP_CID_LOOKUP_SIZE; int err; if (!link_val) return 0; switch (admin->vtype) { case VCAP_TYPE_IS0: /* Add NXT_IDX key for chaining rules between IS0 instances */ err = vcap_rule_add_key_u32(vrule, VCAP_KF_LOOKUP_GEN_IDX_SEL, 1, /* enable */ ~0); if (err) return err; return vcap_rule_add_key_u32(vrule, VCAP_KF_LOOKUP_GEN_IDX, link_val, /* target */ ~0); case VCAP_TYPE_IS2: /* Add PAG key for chaining rules from IS0 */ return vcap_rule_add_key_u32(vrule, VCAP_KF_LOOKUP_PAG, link_val, /* target */ ~0); case VCAP_TYPE_ES0: case VCAP_TYPE_ES2: /* Add ISDX key for chaining rules from IS0 */ return vcap_rule_add_key_u32(vrule, VCAP_KF_ISDX_CLS, link_val, ~0); default: break; } return 0; } /* Add the VCAP action that adds a target value to a rule */ static int sparx5_tc_add_rule_link(struct vcap_control *vctrl, struct vcap_admin *admin, struct vcap_rule *vrule, int from_cid, int to_cid) { struct vcap_admin *to_admin = vcap_find_admin(vctrl, to_cid); int diff, err = 0; if (!to_admin) { pr_err("%s:%d: unsupported chain direction: %d\n", __func__, __LINE__, to_cid); return -EINVAL; } diff = vcap_chain_offset(vctrl, from_cid, to_cid); if (!diff) return 0; if (admin->vtype == VCAP_TYPE_IS0 && to_admin->vtype == VCAP_TYPE_IS0) { /* Between IS0 instances the G_IDX value is used */ err = vcap_rule_add_action_u32(vrule, VCAP_AF_NXT_IDX, diff); if (err) goto out; err = vcap_rule_add_action_u32(vrule, VCAP_AF_NXT_IDX_CTRL, 1); /* Replace */ if (err) goto out; } else if (admin->vtype == VCAP_TYPE_IS0 && to_admin->vtype == VCAP_TYPE_IS2) { /* Between IS0 and IS2 the PAG value is used */ err = vcap_rule_add_action_u32(vrule, VCAP_AF_PAG_VAL, diff); if (err) goto out; err = vcap_rule_add_action_u32(vrule, VCAP_AF_PAG_OVERRIDE_MASK, 0xff); if (err) goto out; } else if (admin->vtype == VCAP_TYPE_IS0 && (to_admin->vtype == VCAP_TYPE_ES0 || to_admin->vtype == VCAP_TYPE_ES2)) { /* Between IS0 and ES0/ES2 the ISDX value is used */ err = vcap_rule_add_action_u32(vrule, VCAP_AF_ISDX_VAL, diff); if (err) goto out; err = vcap_rule_add_action_bit(vrule, VCAP_AF_ISDX_ADD_REPLACE_SEL, VCAP_BIT_1); if (err) goto out; } else { pr_err("%s:%d: unsupported chain destination: %d\n", __func__, __LINE__, to_cid); err = -EOPNOTSUPP; } out: return err; } static int sparx5_tc_flower_parse_act_gate(struct sparx5_psfp_sg *sg, struct flow_action_entry *act, struct netlink_ext_ack *extack) { int i; if (act->gate.prio < -1 || act->gate.prio > SPX5_PSFP_SG_MAX_IPV) { NL_SET_ERR_MSG_MOD(extack, "Invalid gate priority"); return -EINVAL; } if (act->gate.cycletime < SPX5_PSFP_SG_MIN_CYCLE_TIME_NS || act->gate.cycletime > SPX5_PSFP_SG_MAX_CYCLE_TIME_NS) { NL_SET_ERR_MSG_MOD(extack, "Invalid gate cycletime"); return -EINVAL; } if (act->gate.cycletimeext > SPX5_PSFP_SG_MAX_CYCLE_TIME_NS) { NL_SET_ERR_MSG_MOD(extack, "Invalid gate cycletimeext"); return -EINVAL; } if (act->gate.num_entries >= SPX5_PSFP_GCE_CNT) { NL_SET_ERR_MSG_MOD(extack, "Invalid number of gate entries"); return -EINVAL; } sg->gate_state = true; sg->ipv = act->gate.prio; sg->num_entries = act->gate.num_entries; sg->cycletime = act->gate.cycletime; sg->cycletimeext = act->gate.cycletimeext; for (i = 0; i < sg->num_entries; i++) { sg->gce[i].gate_state = !!act->gate.entries[i].gate_state; sg->gce[i].interval = act->gate.entries[i].interval; sg->gce[i].ipv = act->gate.entries[i].ipv; sg->gce[i].maxoctets = act->gate.entries[i].maxoctets; } return 0; } static int sparx5_tc_flower_parse_act_police(struct sparx5_policer *pol, struct flow_action_entry *act, struct netlink_ext_ack *extack) { pol->type = SPX5_POL_SERVICE; pol->rate = div_u64(act->police.rate_bytes_ps, 1000) * 8; pol->burst = act->police.burst; pol->idx = act->hw_index; /* rate is now in kbit */ if (pol->rate > DIV_ROUND_UP(SPX5_SDLB_GROUP_RATE_MAX, 1000)) { NL_SET_ERR_MSG_MOD(extack, "Maximum rate exceeded"); return -EINVAL; } if (act->police.exceed.act_id != FLOW_ACTION_DROP) { NL_SET_ERR_MSG_MOD(extack, "Offload not supported when exceed action is not drop"); return -EOPNOTSUPP; } if (act->police.notexceed.act_id != FLOW_ACTION_PIPE && act->police.notexceed.act_id != FLOW_ACTION_ACCEPT) { NL_SET_ERR_MSG_MOD(extack, "Offload not supported when conform action is not pipe or ok"); return -EOPNOTSUPP; } return 0; } static int sparx5_tc_flower_psfp_setup(struct sparx5 *sparx5, struct vcap_rule *vrule, int sg_idx, int pol_idx, struct sparx5_psfp_sg *sg, struct sparx5_psfp_fm *fm, struct sparx5_psfp_sf *sf) { u32 psfp_sfid = 0, psfp_fmid = 0, psfp_sgid = 0; int ret; /* Must always have a stream gate - max sdu (filter option) is evaluated * after frames have passed the gate, so in case of only a policer, we * allocate a stream gate that is always open. */ if (sg_idx < 0) { sg_idx = sparx5_pool_idx_to_id(SPX5_PSFP_SG_OPEN); sg->ipv = 0; /* Disabled */ sg->cycletime = SPX5_PSFP_SG_CYCLE_TIME_DEFAULT; sg->num_entries = 1; sg->gate_state = 1; /* Open */ sg->gate_enabled = 1; sg->gce[0].gate_state = 1; sg->gce[0].interval = SPX5_PSFP_SG_CYCLE_TIME_DEFAULT; sg->gce[0].ipv = 0; sg->gce[0].maxoctets = 0; /* Disabled */ } ret = sparx5_psfp_sg_add(sparx5, sg_idx, sg, &psfp_sgid); if (ret < 0) return ret; if (pol_idx >= 0) { /* Add new flow-meter */ ret = sparx5_psfp_fm_add(sparx5, pol_idx, fm, &psfp_fmid); if (ret < 0) return ret; } /* Map stream filter to stream gate */ sf->sgid = psfp_sgid; /* Add new stream-filter and map it to a steam gate */ ret = sparx5_psfp_sf_add(sparx5, sf, &psfp_sfid); if (ret < 0) return ret; /* Streams are classified by ISDX - map ISDX 1:1 to sfid for now. */ sparx5_isdx_conf_set(sparx5, psfp_sfid, psfp_sfid, psfp_fmid); ret = vcap_rule_add_action_bit(vrule, VCAP_AF_ISDX_ADD_REPLACE_SEL, VCAP_BIT_1); if (ret) return ret; ret = vcap_rule_add_action_u32(vrule, VCAP_AF_ISDX_VAL, psfp_sfid); if (ret) return ret; return 0; } /* Handle the action trap for a VCAP rule */ static int sparx5_tc_action_trap(struct vcap_admin *admin, struct vcap_rule *vrule, struct flow_cls_offload *fco) { int err = 0; switch (admin->vtype) { case VCAP_TYPE_IS2: err = vcap_rule_add_action_bit(vrule, VCAP_AF_CPU_COPY_ENA, VCAP_BIT_1); if (err) break; err = vcap_rule_add_action_u32(vrule, VCAP_AF_CPU_QUEUE_NUM, 0); if (err) break; err = vcap_rule_add_action_u32(vrule, VCAP_AF_MASK_MODE, SPX5_PMM_REPLACE_ALL); break; case VCAP_TYPE_ES0: err = vcap_rule_add_action_u32(vrule, VCAP_AF_FWD_SEL, SPX5_FWSEL_REDIRECT_TO_LOOPBACK); break; case VCAP_TYPE_ES2: err = vcap_rule_add_action_bit(vrule, VCAP_AF_CPU_COPY_ENA, VCAP_BIT_1); if (err) break; err = vcap_rule_add_action_u32(vrule, VCAP_AF_CPU_QUEUE_NUM, 0); break; default: NL_SET_ERR_MSG_MOD(fco->common.extack, "Trap action not supported in this VCAP"); err = -EOPNOTSUPP; break; } return err; } static int sparx5_tc_action_vlan_pop(struct vcap_admin *admin, struct vcap_rule *vrule, struct flow_cls_offload *fco, u16 tpid) { int err = 0; switch (admin->vtype) { case VCAP_TYPE_ES0: break; default: NL_SET_ERR_MSG_MOD(fco->common.extack, "VLAN pop action not supported in this VCAP"); return -EOPNOTSUPP; } switch (tpid) { case ETH_P_8021Q: case ETH_P_8021AD: err = vcap_rule_add_action_u32(vrule, VCAP_AF_PUSH_OUTER_TAG, SPX5_OTAG_UNTAG); break; default: NL_SET_ERR_MSG_MOD(fco->common.extack, "Invalid vlan proto"); err = -EINVAL; } return err; } static int sparx5_tc_action_vlan_modify(struct vcap_admin *admin, struct vcap_rule *vrule, struct flow_cls_offload *fco, struct flow_action_entry *act, u16 tpid) { int err = 0; switch (admin->vtype) { case VCAP_TYPE_ES0: err = vcap_rule_add_action_u32(vrule, VCAP_AF_PUSH_OUTER_TAG, SPX5_OTAG_TAG_A); if (err) return err; break; default: NL_SET_ERR_MSG_MOD(fco->common.extack, "VLAN modify action not supported in this VCAP"); return -EOPNOTSUPP; } switch (tpid) { case ETH_P_8021Q: err = vcap_rule_add_action_u32(vrule, VCAP_AF_TAG_A_TPID_SEL, SPX5_TPID_A_8100); break; case ETH_P_8021AD: err = vcap_rule_add_action_u32(vrule, VCAP_AF_TAG_A_TPID_SEL, SPX5_TPID_A_88A8); break; default: NL_SET_ERR_MSG_MOD(fco->common.extack, "Invalid vlan proto"); err = -EINVAL; } if (err) return err; err = vcap_rule_add_action_u32(vrule, VCAP_AF_TAG_A_VID_SEL, SPX5_VID_A_VAL); if (err) return err; err = vcap_rule_add_action_u32(vrule, VCAP_AF_VID_A_VAL, act->vlan.vid); if (err) return err; err = vcap_rule_add_action_u32(vrule, VCAP_AF_TAG_A_PCP_SEL, SPX5_PCP_A_VAL); if (err) return err; err = vcap_rule_add_action_u32(vrule, VCAP_AF_PCP_A_VAL, act->vlan.prio); if (err) return err; return vcap_rule_add_action_u32(vrule, VCAP_AF_TAG_A_DEI_SEL, SPX5_DEI_A_CLASSIFIED); } static int sparx5_tc_action_vlan_push(struct vcap_admin *admin, struct vcap_rule *vrule, struct flow_cls_offload *fco, struct flow_action_entry *act, u16 tpid) { u16 act_tpid = be16_to_cpu(act->vlan.proto); int err = 0; switch (admin->vtype) { case VCAP_TYPE_ES0: break; default: NL_SET_ERR_MSG_MOD(fco->common.extack, "VLAN push action not supported in this VCAP"); return -EOPNOTSUPP; } if (tpid == ETH_P_8021AD) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Cannot push on double tagged frames"); return -EOPNOTSUPP; } err = sparx5_tc_action_vlan_modify(admin, vrule, fco, act, act_tpid); if (err) return err; switch (act_tpid) { case ETH_P_8021Q: break; case ETH_P_8021AD: /* Push classified tag as inner tag */ err = vcap_rule_add_action_u32(vrule, VCAP_AF_PUSH_INNER_TAG, SPX5_ITAG_PUSH_B_TAG); if (err) break; err = vcap_rule_add_action_u32(vrule, VCAP_AF_TAG_B_TPID_SEL, SPX5_TPID_B_CLASSIFIED); break; default: NL_SET_ERR_MSG_MOD(fco->common.extack, "Invalid vlan proto"); err = -EINVAL; } return err; } static void sparx5_tc_flower_set_port_mask(struct vcap_u72_action *ports, struct net_device *ndev) { struct sparx5_port *port = netdev_priv(ndev); int byidx = port->portno / BITS_PER_BYTE; int biidx = port->portno % BITS_PER_BYTE; ports->value[byidx] |= BIT(biidx); } static int sparx5_tc_action_mirred(struct vcap_admin *admin, struct vcap_rule *vrule, struct flow_cls_offload *fco, struct flow_action_entry *act) { struct vcap_u72_action ports = {0}; int err; if (admin->vtype != VCAP_TYPE_IS0 && admin->vtype != VCAP_TYPE_IS2) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Mirror action not supported in this VCAP"); return -EOPNOTSUPP; } err = vcap_rule_add_action_u32(vrule, VCAP_AF_MASK_MODE, SPX5_PMM_OR_DSTMASK); if (err) return err; sparx5_tc_flower_set_port_mask(&ports, act->dev); return vcap_rule_add_action_u72(vrule, VCAP_AF_PORT_MASK, &ports); } static int sparx5_tc_action_redirect(struct vcap_admin *admin, struct vcap_rule *vrule, struct flow_cls_offload *fco, struct flow_action_entry *act) { struct vcap_u72_action ports = {0}; int err; if (admin->vtype != VCAP_TYPE_IS0 && admin->vtype != VCAP_TYPE_IS2) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Redirect action not supported in this VCAP"); return -EOPNOTSUPP; } err = vcap_rule_add_action_u32(vrule, VCAP_AF_MASK_MODE, SPX5_PMM_REPLACE_ALL); if (err) return err; sparx5_tc_flower_set_port_mask(&ports, act->dev); return vcap_rule_add_action_u72(vrule, VCAP_AF_PORT_MASK, &ports); } /* Remove rule keys that may prevent templates from matching a keyset */ static void sparx5_tc_flower_simplify_rule(struct vcap_admin *admin, struct vcap_rule *vrule, u16 l3_proto) { switch (admin->vtype) { case VCAP_TYPE_IS0: vcap_rule_rem_key(vrule, VCAP_KF_ETYPE); switch (l3_proto) { case ETH_P_IP: break; case ETH_P_IPV6: vcap_rule_rem_key(vrule, VCAP_KF_IP_SNAP_IS); break; default: break; } break; case VCAP_TYPE_ES2: switch (l3_proto) { case ETH_P_IP: if (vrule->keyset == VCAP_KFS_IP4_OTHER) vcap_rule_rem_key(vrule, VCAP_KF_TCP_IS); break; case ETH_P_IPV6: if (vrule->keyset == VCAP_KFS_IP6_STD) vcap_rule_rem_key(vrule, VCAP_KF_TCP_IS); vcap_rule_rem_key(vrule, VCAP_KF_IP4_IS); break; default: break; } break; case VCAP_TYPE_IS2: switch (l3_proto) { case ETH_P_IP: case ETH_P_IPV6: vcap_rule_rem_key(vrule, VCAP_KF_IP4_IS); break; default: break; } break; default: break; } } static bool sparx5_tc_flower_use_template(struct net_device *ndev, struct flow_cls_offload *fco, struct vcap_admin *admin, struct vcap_rule *vrule) { struct sparx5_port *port = netdev_priv(ndev); struct sparx5_tc_flower_template *ftp; list_for_each_entry(ftp, &port->tc_templates, list) { if (ftp->cid != fco->common.chain_index) continue; vcap_set_rule_set_keyset(vrule, ftp->keyset); sparx5_tc_flower_simplify_rule(admin, vrule, ftp->l3_proto); return true; } return false; } static int sparx5_tc_flower_replace(struct net_device *ndev, struct flow_cls_offload *fco, struct vcap_admin *admin, bool ingress) { struct sparx5_psfp_sf sf = { .max_sdu = SPX5_PSFP_SF_MAX_SDU }; struct netlink_ext_ack *extack = fco->common.extack; int err, idx, tc_sg_idx = -1, tc_pol_idx = -1; struct vcap_tc_flower_parse_usage state = { .fco = fco, .l3_proto = ETH_P_ALL, .admin = admin, }; struct sparx5_port *port = netdev_priv(ndev); struct sparx5_multiple_rules multi = {}; struct sparx5 *sparx5 = port->sparx5; struct sparx5_psfp_sg sg = { 0 }; struct sparx5_psfp_fm fm = { 0 }; struct flow_action_entry *act; struct vcap_control *vctrl; struct flow_rule *frule; struct vcap_rule *vrule; vctrl = port->sparx5->vcap_ctrl; err = sparx5_tc_flower_action_check(vctrl, ndev, fco, ingress); if (err) return err; vrule = vcap_alloc_rule(vctrl, ndev, fco->common.chain_index, VCAP_USER_TC, fco->common.prio, 0); if (IS_ERR(vrule)) return PTR_ERR(vrule); vrule->cookie = fco->cookie; state.vrule = vrule; state.frule = flow_cls_offload_flow_rule(fco); err = sparx5_tc_use_dissectors(&state, admin, vrule); if (err) goto out; err = sparx5_tc_add_rule_counter(admin, vrule); if (err) goto out; err = sparx5_tc_add_rule_link_target(admin, vrule, fco->common.chain_index); if (err) goto out; frule = flow_cls_offload_flow_rule(fco); flow_action_for_each(idx, act, &frule->action) { switch (act->id) { case FLOW_ACTION_GATE: { err = sparx5_tc_flower_parse_act_gate(&sg, act, extack); if (err < 0) goto out; tc_sg_idx = act->hw_index; break; } case FLOW_ACTION_POLICE: { err = sparx5_tc_flower_parse_act_police(&fm.pol, act, extack); if (err < 0) goto out; tc_pol_idx = fm.pol.idx; sf.max_sdu = act->police.mtu; break; } case FLOW_ACTION_TRAP: err = sparx5_tc_action_trap(admin, vrule, fco); if (err) goto out; break; case FLOW_ACTION_MIRRED: err = sparx5_tc_action_mirred(admin, vrule, fco, act); if (err) goto out; break; case FLOW_ACTION_REDIRECT: err = sparx5_tc_action_redirect(admin, vrule, fco, act); if (err) goto out; break; case FLOW_ACTION_ACCEPT: err = sparx5_tc_set_actionset(admin, vrule); if (err) goto out; break; case FLOW_ACTION_GOTO: err = sparx5_tc_set_actionset(admin, vrule); if (err) goto out; sparx5_tc_add_rule_link(vctrl, admin, vrule, fco->common.chain_index, act->chain_index); break; case FLOW_ACTION_VLAN_POP: err = sparx5_tc_action_vlan_pop(admin, vrule, fco, state.tpid); if (err) goto out; break; case FLOW_ACTION_VLAN_PUSH: err = sparx5_tc_action_vlan_push(admin, vrule, fco, act, state.tpid); if (err) goto out; break; case FLOW_ACTION_VLAN_MANGLE: err = sparx5_tc_action_vlan_modify(admin, vrule, fco, act, state.tpid); if (err) goto out; break; default: NL_SET_ERR_MSG_MOD(fco->common.extack, "Unsupported TC action"); err = -EOPNOTSUPP; goto out; } } /* Setup PSFP */ if (tc_sg_idx >= 0 || tc_pol_idx >= 0) { err = sparx5_tc_flower_psfp_setup(sparx5, vrule, tc_sg_idx, tc_pol_idx, &sg, &fm, &sf); if (err) goto out; } if (!sparx5_tc_flower_use_template(ndev, fco, admin, vrule)) { err = sparx5_tc_select_protocol_keyset(ndev, vrule, admin, state.l3_proto, &multi); if (err) { NL_SET_ERR_MSG_MOD(fco->common.extack, "No matching port keyset for filter protocol and keys"); goto out; } } /* provide the l3 protocol to guide the keyset selection */ err = vcap_val_rule(vrule, state.l3_proto); if (err) { vcap_set_tc_exterr(fco, vrule); goto out; } err = vcap_add_rule(vrule); if (err) NL_SET_ERR_MSG_MOD(fco->common.extack, "Could not add the filter"); if (state.l3_proto == ETH_P_ALL) err = sparx5_tc_add_remaining_rules(vctrl, fco, vrule, admin, &multi); out: vcap_free_rule(vrule); return err; } static void sparx5_tc_free_psfp_resources(struct sparx5 *sparx5, struct vcap_rule *vrule) { struct vcap_client_actionfield *afield; u32 isdx, sfid, sgid, fmid; /* Check if VCAP_AF_ISDX_VAL action is set for this rule - and if * it is used for stream and/or flow-meter classification. */ afield = vcap_find_actionfield(vrule, VCAP_AF_ISDX_VAL); if (!afield) return; isdx = afield->data.u32.value; sfid = sparx5_psfp_isdx_get_sf(sparx5, isdx); if (!sfid) return; fmid = sparx5_psfp_isdx_get_fm(sparx5, isdx); sgid = sparx5_psfp_sf_get_sg(sparx5, sfid); if (fmid && sparx5_psfp_fm_del(sparx5, fmid) < 0) pr_err("%s:%d Could not delete invalid fmid: %d", __func__, __LINE__, fmid); if (sgid && sparx5_psfp_sg_del(sparx5, sgid) < 0) pr_err("%s:%d Could not delete invalid sgid: %d", __func__, __LINE__, sgid); if (sparx5_psfp_sf_del(sparx5, sfid) < 0) pr_err("%s:%d Could not delete invalid sfid: %d", __func__, __LINE__, sfid); sparx5_isdx_conf_set(sparx5, isdx, 0, 0); } static int sparx5_tc_free_rule_resources(struct net_device *ndev, struct vcap_control *vctrl, int rule_id) { struct sparx5_port *port = netdev_priv(ndev); struct sparx5 *sparx5 = port->sparx5; struct vcap_rule *vrule; int ret = 0; vrule = vcap_get_rule(vctrl, rule_id); if (IS_ERR(vrule)) return -EINVAL; sparx5_tc_free_psfp_resources(sparx5, vrule); vcap_free_rule(vrule); return ret; } static int sparx5_tc_flower_destroy(struct net_device *ndev, struct flow_cls_offload *fco, struct vcap_admin *admin) { struct sparx5_port *port = netdev_priv(ndev); int err = -ENOENT, count = 0, rule_id; struct vcap_control *vctrl; vctrl = port->sparx5->vcap_ctrl; while (true) { rule_id = vcap_lookup_rule_by_cookie(vctrl, fco->cookie); if (rule_id <= 0) break; if (count == 0) { /* Resources are attached to the first rule of * a set of rules. Only works if the rules are * in the correct order. */ err = sparx5_tc_free_rule_resources(ndev, vctrl, rule_id); if (err) pr_err("%s:%d: could not free resources %d\n", __func__, __LINE__, rule_id); } err = vcap_del_rule(vctrl, ndev, rule_id); if (err) { pr_err("%s:%d: could not delete rule %d\n", __func__, __LINE__, rule_id); break; } } return err; } static int sparx5_tc_flower_stats(struct net_device *ndev, struct flow_cls_offload *fco, struct vcap_admin *admin) { struct sparx5_port *port = netdev_priv(ndev); struct vcap_counter ctr = {}; struct vcap_control *vctrl; ulong lastused = 0; int err; vctrl = port->sparx5->vcap_ctrl; err = vcap_get_rule_count_by_cookie(vctrl, &ctr, fco->cookie); if (err) return err; flow_stats_update(&fco->stats, 0x0, ctr.value, 0, lastused, FLOW_ACTION_HW_STATS_IMMEDIATE); return err; } static int sparx5_tc_flower_template_create(struct net_device *ndev, struct flow_cls_offload *fco, struct vcap_admin *admin) { struct sparx5_port *port = netdev_priv(ndev); struct vcap_tc_flower_parse_usage state = { .fco = fco, .l3_proto = ETH_P_ALL, .admin = admin, }; struct sparx5_tc_flower_template *ftp; struct vcap_keyset_list kslist = {}; enum vcap_keyfield_set keysets[10]; struct vcap_control *vctrl; struct vcap_rule *vrule; int count, err; if (admin->vtype == VCAP_TYPE_ES0) { pr_err("%s:%d: %s\n", __func__, __LINE__, "VCAP does not support templates"); return -EINVAL; } count = vcap_admin_rule_count(admin, fco->common.chain_index); if (count > 0) { pr_err("%s:%d: %s\n", __func__, __LINE__, "Filters are already present"); return -EBUSY; } ftp = kzalloc(sizeof(*ftp), GFP_KERNEL); if (!ftp) return -ENOMEM; ftp->cid = fco->common.chain_index; ftp->orig = VCAP_KFS_NO_VALUE; ftp->keyset = VCAP_KFS_NO_VALUE; vctrl = port->sparx5->vcap_ctrl; vrule = vcap_alloc_rule(vctrl, ndev, fco->common.chain_index, VCAP_USER_TC, fco->common.prio, 0); if (IS_ERR(vrule)) { err = PTR_ERR(vrule); goto err_rule; } state.vrule = vrule; state.frule = flow_cls_offload_flow_rule(fco); err = sparx5_tc_use_dissectors(&state, admin, vrule); if (err) { pr_err("%s:%d: key error: %d\n", __func__, __LINE__, err); goto out; } ftp->l3_proto = state.l3_proto; sparx5_tc_flower_simplify_rule(admin, vrule, state.l3_proto); /* Find the keysets that the rule can use */ kslist.keysets = keysets; kslist.max = ARRAY_SIZE(keysets); if (!vcap_rule_find_keysets(vrule, &kslist)) { pr_err("%s:%d: %s\n", __func__, __LINE__, "Could not find a suitable keyset"); err = -ENOENT; goto out; } ftp->keyset = vcap_select_min_rule_keyset(vctrl, admin->vtype, &kslist); kslist.cnt = 0; sparx5_vcap_set_port_keyset(ndev, admin, fco->common.chain_index, state.l3_proto, ftp->keyset, &kslist); if (kslist.cnt > 0) ftp->orig = kslist.keysets[0]; /* Store new template */ list_add_tail(&ftp->list, &port->tc_templates); vcap_free_rule(vrule); return 0; out: vcap_free_rule(vrule); err_rule: kfree(ftp); return err; } static int sparx5_tc_flower_template_destroy(struct net_device *ndev, struct flow_cls_offload *fco, struct vcap_admin *admin) { struct sparx5_port *port = netdev_priv(ndev); struct sparx5_tc_flower_template *ftp, *tmp; int err = -ENOENT; /* Rules using the template are removed by the tc framework */ list_for_each_entry_safe(ftp, tmp, &port->tc_templates, list) { if (ftp->cid != fco->common.chain_index) continue; sparx5_vcap_set_port_keyset(ndev, admin, fco->common.chain_index, ftp->l3_proto, ftp->orig, NULL); list_del(&ftp->list); kfree(ftp); break; } return err; } int sparx5_tc_flower(struct net_device *ndev, struct flow_cls_offload *fco, bool ingress) { struct sparx5_port *port = netdev_priv(ndev); struct vcap_control *vctrl; struct vcap_admin *admin; int err = -EINVAL; /* Get vcap instance from the chain id */ vctrl = port->sparx5->vcap_ctrl; admin = vcap_find_admin(vctrl, fco->common.chain_index); if (!admin) { NL_SET_ERR_MSG_MOD(fco->common.extack, "Invalid chain"); return err; } switch (fco->command) { case FLOW_CLS_REPLACE: return sparx5_tc_flower_replace(ndev, fco, admin, ingress); case FLOW_CLS_DESTROY: return sparx5_tc_flower_destroy(ndev, fco, admin); case FLOW_CLS_STATS: return sparx5_tc_flower_stats(ndev, fco, admin); case FLOW_CLS_TMPLT_CREATE: return sparx5_tc_flower_template_create(ndev, fco, admin); case FLOW_CLS_TMPLT_DESTROY: return sparx5_tc_flower_template_destroy(ndev, fco, admin); default: return -EOPNOTSUPP; } }
Information contained on this website is for historical information purposes only and does not indicate or represent copyright ownership.
Created with Cregit http://github.com/cregit/cregit
Version 2.0-RC1