#include #include #include #include #include #include #include #include #include #include "ila.h" struct ila_xlat_params { struct ila_params ip; __be64 identifier; int ifindex; unsigned int dir; }; struct ila_map { struct ila_xlat_params p; struct rhash_head node; struct ila_map __rcu *next; struct rcu_head rcu; }; static unsigned int ila_net_id; struct ila_net { struct rhashtable rhash_table; spinlock_t *locks; /* Bucket locks for entry manipulation */ unsigned int locks_mask; bool hooks_registered; }; #define LOCKS_PER_CPU 10 static int alloc_ila_locks(struct ila_net *ilan) { unsigned int i, size; unsigned int nr_pcpus = num_possible_cpus(); nr_pcpus = min_t(unsigned int, nr_pcpus, 32UL); size = roundup_pow_of_two(nr_pcpus * LOCKS_PER_CPU); if (sizeof(spinlock_t) != 0) { #ifdef CONFIG_NUMA if (size * sizeof(spinlock_t) > PAGE_SIZE) ilan->locks = vmalloc(size * sizeof(spinlock_t)); else #endif ilan->locks = kmalloc_array(size, sizeof(spinlock_t), GFP_KERNEL); if (!ilan->locks) return -ENOMEM; for (i = 0; i < size; i++) spin_lock_init(&ilan->locks[i]); } ilan->locks_mask = size - 1; return 0; } static u32 hashrnd __read_mostly; static __always_inline void __ila_hash_secret_init(void) { net_get_random_once(&hashrnd, sizeof(hashrnd)); } static inline u32 ila_identifier_hash(__be64 identifier) { u32 *v = (u32 *)&identifier; return jhash_2words(v[0], v[1], hashrnd); } static inline spinlock_t *ila_get_lock(struct ila_net *ilan, __be64 identifier) { return &ilan->locks[ila_identifier_hash(identifier) & ilan->locks_mask]; } static inline int ila_cmp_wildcards(struct ila_map *ila, __be64 loc, int ifindex, unsigned int dir) { return (ila->p.ip.locator_match && ila->p.ip.locator_match != loc) || (ila->p.ifindex && ila->p.ifindex != ifindex) || !(ila->p.dir & dir); } static inline int ila_cmp_params(struct ila_map *ila, struct ila_xlat_params *p) { return (ila->p.ip.locator_match != p->ip.locator_match) || (ila->p.ifindex != p->ifindex) || (ila->p.dir != p->dir); } static int ila_cmpfn(struct rhashtable_compare_arg *arg, const void *obj) { const struct ila_map *ila = obj; return (ila->p.identifier != *(__be64 *)arg->key); } static inline int ila_order(struct ila_map *ila) { int score = 0; if (ila->p.ip.locator_match) score += 1 << 0; if (ila->p.ifindex) score += 1 << 1; return score; } static const struct rhashtable_params rht_params = { .nelem_hint = 1024, .head_offset = offsetof(struct ila_map, node), .key_offset = offsetof(struct ila_map, p.identifier), .key_len = sizeof(u64), /* identifier */ .max_size = 1048576, .min_size = 256, .automatic_shrinking = true, .obj_cmpfn = ila_cmpfn, }; static struct genl_family ila_nl_family = { .id = GENL_ID_GENERATE, .hdrsize = 0, .name = ILA_GENL_NAME, .version = ILA_GENL_VERSION, .maxattr = ILA_ATTR_MAX, .netnsok = true, .parallel_ops = true, }; static struct nla_policy ila_nl_policy[ILA_ATTR_MAX + 1] = { [ILA_ATTR_IDENTIFIER] = { .type = NLA_U64, }, [ILA_ATTR_LOCATOR] = { .type = NLA_U64, }, [ILA_ATTR_LOCATOR_MATCH] = { .type = NLA_U64, }, [ILA_ATTR_IFINDEX] = { .type = NLA_U32, }, [ILA_ATTR_DIR] = { .type = NLA_U32, }, }; static int parse_nl_config(struct genl_info *info, struct ila_xlat_params *p) { memset(p, 0, sizeof(*p)); if (info->attrs[ILA_ATTR_IDENTIFIER]) p->identifier = (__force __be64)nla_get_u64( info->attrs[ILA_ATTR_IDENTIFIER]); if (info->attrs[ILA_ATTR_LOCATOR]) p->ip.locator = (__force __be64)nla_get_u64( info->attrs[ILA_ATTR_LOCATOR]); if (info->attrs[ILA_ATTR_LOCATOR_MATCH]) p->ip.locator_match = (__force __be64)nla_get_u64( info->attrs[ILA_ATTR_LOCATOR_MATCH]); if (info->attrs[ILA_ATTR_IFINDEX]) p->ifindex = nla_get_s32(info->attrs[ILA_ATTR_IFINDEX]); if (info->attrs[ILA_ATTR_DIR]) p->dir = nla_get_u32(info->attrs[ILA_ATTR_DIR]); return 0; } /* Must be called with rcu readlock */ static inline struct ila_map *ila_lookup_wildcards(__be64 id, __be64 loc, int ifindex, unsigned int dir, struct ila_net *ilan) { struct ila_map *ila; ila = rhashtable_lookup_fast(&ilan->rhash_table, &id, rht_params); while (ila) { if (!ila_cmp_wildcards(ila, loc, ifindex, dir)) return ila; ila = rcu_access_pointer(ila->next); } return NULL; } /* Must be called with rcu readlock */ static inline struct ila_map *ila_lookup_by_params(struct ila_xlat_params *p, struct ila_net *ilan) { struct ila_map *ila; ila = rhashtable_lookup_fast(&ilan->rhash_table, &p->identifier, rht_params); while (ila) { if (!ila_cmp_params(ila, p)) return ila; ila = rcu_access_pointer(ila->next); } return NULL; } static inline void ila_release(struct ila_map *ila) { kfree_rcu(ila, rcu); } static void ila_free_cb(void *ptr, void *arg) { struct ila_map *ila = (struct ila_map *)ptr, *next; /* Assume rcu_readlock held */ while (ila) { next = rcu_access_pointer(ila->next); ila_release(ila); ila = next; } } static int ila_xlat_addr(struct sk_buff *skb, int dir); static unsigned int ila_nf_input(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { ila_xlat_addr(skb, ILA_DIR_IN); return NF_ACCEPT; } static struct nf_hook_ops ila_nf_hook_ops[] __read_mostly = { { .hook = ila_nf_input, .pf = NFPROTO_IPV6, .hooknum = NF_INET_PRE_ROUTING, .priority = -1, }, }; static int ila_add_mapping(struct net *net, struct ila_xlat_params *p) { struct ila_net *ilan = net_generic(net, ila_net_id); struct ila_map *ila, *head; spinlock_t *lock = ila_get_lock(ilan, p->identifier); int err = 0, order; if (!ilan->hooks_registered) { /* We defer registering net hooks in the namespace until the * first mapping is added. */ err = nf_register_net_hooks(net, ila_nf_hook_ops, ARRAY_SIZE(ila_nf_hook_ops)); if (err) return err; ilan->hooks_registered = true; } ila = kzalloc(sizeof(*ila), GFP_KERNEL); if (!ila) return -ENOMEM; ila->p = *p; if (p->ip.locator_match) { /* Precompute checksum difference for translation since we * know both the old identifier and the new one. */ ila->p.ip.csum_diff = compute_csum_diff8( (__be32 *)&p->ip.locator_match, (__be32 *)&p->ip.locator); } order = ila_order(ila); spin_lock(lock); head = rhashtable_lookup_fast(&ilan->rhash_table, &p->identifier, rht_params); if (!head) { /* New entry for the rhash_table */ err = rhashtable_lookup_insert_fast(&ilan->rhash_table, &ila->node, rht_params); } else { struct ila_map *tila = head, *prev = NULL; do { if (!ila_cmp_params(tila, p)) { err = -EEXIST; goto out; } if (order > ila_order(tila)) break; prev = tila; tila = rcu_dereference_protected(tila->next, lockdep_is_held(lock)); } while (tila); if (prev) { /* Insert in sub list of head */ RCU_INIT_POINTER(ila->next, tila); rcu_assign_pointer(prev->next, ila); } else { /* Make this ila new head */ RCU_INIT_POINTER(ila->next, head); err = rhashtable_replace_fast(&ilan->rhash_table, &head->node, &ila->node, rht_params); if (err) goto out; } } out: spin_unlock(lock); if (err) kfree(ila); return err; } static int ila_del_mapping(struct net *net, struct ila_xlat_params *p) { struct ila_net *ilan = net_generic(net, ila_net_id); struct ila_map *ila, *head, *prev; spinlock_t *lock = ila_get_lock(ilan, p->identifier); int err = -ENOENT; spin_lock(lock); head = rhashtable_lookup_fast(&ilan->rhash_table, &p->identifier, rht_params); ila = head; prev = NULL; while (ila) { if (ila_cmp_params(ila, p)) { prev = ila; ila = rcu_dereference_protected(ila->next, lockdep_is_held(lock)); continue; } err = 0; if (prev) { /* Not head, just delete from list */ rcu_assign_pointer(prev->next, ila->next); } else { /* It is the head. If there is something in the * sublist we need to make a new head. */ head = rcu_dereference_protected(ila->next, lockdep_is_held(lock)); if (head) { /* Put first entry in the sublist into the * table */ err = rhashtable_replace_fast( &ilan->rhash_table, &ila->node, &head->node, rht_params); if (err) goto out; } else { /* Entry no longer used */ err = rhashtable_remove_fast(&ilan->rhash_table, &ila->node, rht_params); } } ila_release(ila); break; } out: spin_unlock(lock); return err; } static int ila_nl_cmd_add_mapping(struct sk_buff *skb, struct genl_info *info) { struct net *net = genl_info_net(info); struct ila_xlat_params p; int err; err = parse_nl_config(info, &p); if (err) return err; return ila_add_mapping(net, &p); } static int ila_nl_cmd_del_mapping(struct sk_buff *skb, struct genl_info *info) { struct net *net = genl_info_net(info); struct ila_xlat_params p; int err; err = parse_nl_config(info, &p); if (err) return err; ila_del_mapping(net, &p); return 0; } static int ila_fill_info(struct ila_map *ila, struct sk_buff *msg) { if (nla_put_u64(msg, ILA_ATTR_IDENTIFIER, (__force u64)ila->p.identifier) || nla_put_u64(msg, ILA_ATTR_LOCATOR, (__force u64)ila->p.ip.locator) || nla_put_u64(msg, ILA_ATTR_LOCATOR_MATCH, (__force u64)ila->p.ip.locator_match) || nla_put_s32(msg, ILA_ATTR_IFINDEX, ila->p.ifindex) || nla_put_u32(msg, ILA_ATTR_DIR, ila->p.dir)) return -1; return 0; } static int ila_dump_info(struct ila_map *ila, u32 portid, u32 seq, u32 flags, struct sk_buff *skb, u8 cmd) { void *hdr; hdr = genlmsg_put(skb, portid, seq, &ila_nl_family, flags, cmd); if (!hdr) return -ENOMEM; if (ila_fill_info(ila, skb) < 0) goto nla_put_failure; genlmsg_end(skb, hdr); return 0; nla_put_failure: genlmsg_cancel(skb, hdr); return -EMSGSIZE; } static int ila_nl_cmd_get_mapping(struct sk_buff *skb, struct genl_info *info) { struct net *net = genl_info_net(info); struct ila_net *ilan = net_generic(net, ila_net_id); struct sk_buff *msg; struct ila_xlat_params p; struct ila_map *ila; int ret; ret = parse_nl_config(info, &p); if (ret) return ret; msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL); if (!msg) return -ENOMEM; rcu_read_lock(); ila = ila_lookup_by_params(&p, ilan); if (ila) { ret = ila_dump_info(ila, info->snd_portid, info->snd_seq, 0, msg, info->genlhdr->cmd); } rcu_read_unlock(); if (ret < 0) goto out_free; return genlmsg_reply(msg, info); out_free: nlmsg_free(msg); return ret; } struct ila_dump_iter { struct rhashtable_iter rhiter; }; static int ila_nl_dump_start(struct netlink_callback *cb) { struct net *net = sock_net(cb->skb->sk); struct ila_net *ilan = net_generic(net, ila_net_id); struct ila_dump_iter *iter = (struct ila_dump_iter *)cb->args; return rhashtable_walk_init(&ilan->rhash_table, &iter->rhiter); } static int ila_nl_dump_done(struct netlink_callback *cb) { struct ila_dump_iter *iter = (struct ila_dump_iter *)cb->args; rhashtable_walk_exit(&iter->rhiter); return 0; } static int ila_nl_dump(struct sk_buff *skb, struct netlink_callback *cb) { struct ila_dump_iter *iter = (struct ila_dump_iter *)cb->args; struct rhashtable_iter *rhiter = &iter->rhiter; struct ila_map *ila; int ret; ret = rhashtable_walk_start(rhiter); if (ret && ret != -EAGAIN) goto done; for (;;) { ila = rhashtable_walk_next(rhiter); if (IS_ERR(ila)) { if (PTR_ERR(ila) == -EAGAIN) continue; ret = PTR_ERR(ila); goto done; } else if (!ila) { break; } while (ila) { ret = ila_dump_info(ila, NETLINK_CB(cb->skb).portid, cb->nlh->nlmsg_seq, NLM_F_MULTI, skb, ILA_CMD_GET); if (ret) goto done; ila = rcu_access_pointer(ila->next); } } ret = skb->len; done: rhashtable_walk_stop(rhiter); return ret; } static const struct genl_ops ila_nl_ops[] = { { .cmd = ILA_CMD_ADD, .doit = ila_nl_cmd_add_mapping, .policy = ila_nl_policy, .flags = GENL_ADMIN_PERM, }, { .cmd = ILA_CMD_DEL, .doit = ila_nl_cmd_del_mapping, .policy = ila_nl_policy, .flags = GENL_ADMIN_PERM, }, { .cmd = ILA_CMD_GET, .doit = ila_nl_cmd_get_mapping, .start = ila_nl_dump_start, .dumpit = ila_nl_dump, .done = ila_nl_dump_done, .policy = ila_nl_policy, }, }; #define ILA_HASH_TABLE_SIZE 1024 static __net_init int ila_init_net(struct net *net) { int err; struct ila_net *ilan = net_generic(net, ila_net_id); err = alloc_ila_locks(ilan); if (err) return err; rhashtable_init(&ilan->rhash_table, &rht_params); return 0; } static __net_exit void ila_exit_net(struct net *net) { struct ila_net *ilan = net_generic(net, ila_net_id); rhashtable_free_and_destroy(&ilan->rhash_table, ila_free_cb, NULL); kvfree(ilan->locks); if (ilan->hooks_registered) nf_unregister_net_hooks(net, ila_nf_hook_ops, ARRAY_SIZE(ila_nf_hook_ops)); } static struct pernet_operations ila_net_ops = { .init = ila_init_net, .exit = ila_exit_net, .id = &ila_net_id, .size = sizeof(struct ila_net), }; static int ila_xlat_addr(struct sk_buff *skb, int dir) { struct ila_map *ila; struct ipv6hdr *ip6h = ipv6_hdr(skb); struct net *net = dev_net(skb->dev); struct ila_net *ilan = net_generic(net, ila_net_id); __be64 identifier, locator_match; size_t nhoff; /* Assumes skb contains a valid IPv6 header that is pulled */ identifier = *(__be64 *)&ip6h->daddr.in6_u.u6_addr8[8]; locator_match = *(__be64 *)&ip6h->daddr.in6_u.u6_addr8[0]; nhoff = sizeof(struct ipv6hdr); rcu_read_lock(); ila = ila_lookup_wildcards(identifier, locator_match, skb->dev->ifindex, dir, ilan); if (ila) update_ipv6_locator(skb, &ila->p.ip); rcu_read_unlock(); return 0; } int ila_xlat_incoming(struct sk_buff *skb) { return ila_xlat_addr(skb, ILA_DIR_IN); } EXPORT_SYMBOL(ila_xlat_incoming); int ila_xlat_outgoing(struct sk_buff *skb) { return ila_xlat_addr(skb, ILA_DIR_OUT); } EXPORT_SYMBOL(ila_xlat_outgoing); int ila_xlat_init(void) { int ret; ret = register_pernet_device(&ila_net_ops); if (ret) goto exit; ret = genl_register_family_with_ops(&ila_nl_family, ila_nl_ops); if (ret < 0) goto unregister; return 0; unregister: unregister_pernet_device(&ila_net_ops); exit: return ret; } void ila_xlat_fini(void) { genl_unregister_family(&ila_nl_family); unregister_pernet_device(&ila_net_ops); }