diff --git a/include/net/ipv6.h b/include/net/ipv6.h
index e36c2c1..41da032 100644
--- a/include/net/ipv6.h
+++ b/include/net/ipv6.h
@@ -386,13 +386,60 @@ struct ipv6_txoptions *ipv6_fixup_options(struct ipv6_txoptions *opt_space,
  *     and false, if it failed.
  *     It MUST NOT touch skb->h.
  */
-struct tlvtype_proc {
-	int	type;
-	bool	(*func)(struct sk_buff *skb, int offset);
+struct tlv_ops {
+	bool	(*func)(unsigned int class, struct sk_buff *skb, int offset);
 };
 
-extern const struct tlvtype_proc tlvprocdestopt_lst[];
-extern const struct tlvtype_proc tlvprochopopt_lst[];
+struct tlv_params {
+	unsigned char rx_class : 3;
+};
+
+struct tlv_proc {
+	struct tlv_ops ops;
+	struct tlv_params params;
+};
+
+struct tlv_proc_init {
+	int type;
+	struct tlv_proc proc;
+};
+
+struct tlv_param_table_data {
+	unsigned char entries[256];
+	unsigned char count;
+	struct rcu_head rcu;
+	struct tlv_proc procs[0];
+};
+
+struct tlv_param_table {
+	struct tlv_param_table_data __rcu *data;
+};
+
+extern struct tlv_param_table ipv6_tlv_param_table;
+
+int tlv_set_proc(struct tlv_param_table *tlv_param_table,
+		 unsigned char type, const struct tlv_proc *proc);
+int tlv_unset_proc(struct tlv_param_table *tlv_param_table, unsigned char type);
+int tlv_set_params(struct tlv_param_table *tlv_param_table,
+		   unsigned char type, const struct tlv_params *params);
+int tlv_unset_params(struct tlv_param_table *tlv_param_table,
+		     unsigned char type);
+
+int exthdrs_init(struct tlv_param_table *tlv_param_table,
+		 const struct tlv_proc_init *init_params,
+		 int num_init_params);
+void exthdrs_fini(struct tlv_param_table *tlv_param_table);
+
+/* tlv_get_proc assumes rcu_read_lock is held */
+static inline struct tlv_proc *tlv_get_proc(
+		struct tlv_param_table *tlv_param_table,
+		unsigned int type)
+{
+	struct tlv_param_table_data *tpt =
+				rcu_dereference(tlv_param_table->data);
+
+	return &tpt->procs[tpt->entries[type]];
+}
 
 bool ipv6_opt_accepted(const struct sock *sk, const struct sk_buff *skb,
 		       const struct inet6_skb_parm *opt);
diff --git a/include/uapi/linux/in6.h b/include/uapi/linux/in6.h
index 9f2273a..8b9ac7f 100644
--- a/include/uapi/linux/in6.h
+++ b/include/uapi/linux/in6.h
@@ -297,4 +297,14 @@ struct in6_flowlabel_req {
  * ...
  * MRT6_MAX
  */
+
+/* Flags for EH type that can use a TLV option */
+#define IPV6_TLV_CLASS_FLAG_HOPOPT	BIT(0)
+#define IPV6_TLV_CLASS_FLAG_RTRDSTOPT	BIT(1)
+#define IPV6_TLV_CLASS_FLAG_DSTOPT	BIT(2)
+#define IPV6_TLV_CLASS_MAX		((1 << 3) - 1)
+
+#define IPV6_TLV_CLASS_ANY_DSTOPT      (IPV6_TLV_CLASS_FLAG_RTRDSTOPT | \
+					IPV6_TLV_CLASS_FLAG_DSTOPT)
+
 #endif /* _UAPI_LINUX_IN6_H */
diff --git a/net/ipv6/exthdrs.c b/net/ipv6/exthdrs.c
index 6dbacf1..b28c108 100644
--- a/net/ipv6/exthdrs.c
+++ b/net/ipv6/exthdrs.c
@@ -100,15 +100,14 @@ static bool ip6_tlvopt_unknown(struct sk_buff *skb, int optoff,
 
 /* Parse tlv encoded option header (hop-by-hop or destination) */
 
-static bool ip6_parse_tlv(const struct tlvtype_proc *procs,
-			  struct sk_buff *skb,
+static bool ip6_parse_tlv(unsigned int class, struct sk_buff *skb,
 			  int max_count)
 {
 	int len = (skb_transport_header(skb)[1] + 1) << 3;
 	const unsigned char *nh = skb_network_header(skb);
 	int off = skb_network_header_len(skb);
-	const struct tlvtype_proc *curr;
 	bool disallow_unknowns = false;
+	const struct tlv_proc *curr;
 	int tlv_count = 0;
 	int padlen = 0;
 
@@ -117,12 +116,16 @@ static bool ip6_parse_tlv(const struct tlvtype_proc *procs,
 		max_count = -max_count;
 	}
 
-	if (skb_transport_offset(skb) + len > skb_headlen(skb))
-		goto bad;
+	if (skb_transport_offset(skb) + len > skb_headlen(skb)) {
+		kfree_skb(skb);
+		return false;
+	}
 
 	off += 2;
 	len -= 2;
 
+	rcu_read_lock();
+
 	while (len > 0) {
 		int optlen = nh[off + 1] + 2;
 		int i;
@@ -162,19 +165,18 @@ static bool ip6_parse_tlv(const struct tlvtype_proc *procs,
 			if (tlv_count > max_count)
 				goto bad;
 
-			for (curr = procs; curr->type >= 0; curr++) {
-				if (curr->type == nh[off]) {
-					/* type specific length/alignment
-					   checks will be performed in the
-					   func(). */
-					if (curr->func(skb, off) == false)
-						return false;
-					break;
-				}
+			curr = tlv_get_proc(&ipv6_tlv_param_table, nh[off]);
+			if ((curr->params.rx_class & class) && curr->ops.func) {
+				/* Handler will apply additional checks to
+				 * the TLV
+				 */
+				if (!curr->ops.func(class, skb, off))
+					goto bad_nofree;
+			} else if (!ip6_tlvopt_unknown(skb, off,
+						       disallow_unknowns)) {
+				/* No appropriate handler, TLV is unknown */
+				goto bad_nofree;
 			}
-			if (curr->type < 0 &&
-			    !ip6_tlvopt_unknown(skb, off, disallow_unknowns))
-				return false;
 
 			padlen = 0;
 			break;
@@ -183,10 +185,14 @@ static bool ip6_parse_tlv(const struct tlvtype_proc *procs,
 		len -= optlen;
 	}
 
-	if (len == 0)
+	if (len == 0) {
+		rcu_read_unlock();
 		return true;
+	}
 bad:
 	kfree_skb(skb);
+bad_nofree:
+	rcu_read_unlock();
 	return false;
 }
 
@@ -220,7 +226,7 @@ static int ipv6_destopt_rcv(struct sk_buff *skb)
 	dstbuf = opt->dst1;
 #endif
 
-	if (ip6_parse_tlv(tlvprocdestopt_lst, skb,
+	if (ip6_parse_tlv(IPV6_TLV_CLASS_FLAG_DSTOPT, skb,
 			  init_net.ipv6.sysctl.max_dst_opts_cnt)) {
 		skb->transport_header += extlen;
 		opt = IP6CB(skb);
@@ -643,7 +649,7 @@ int ipv6_parse_hopopts(struct sk_buff *skb)
 		goto fail_and_free;
 
 	opt->flags |= IP6SKB_HOPBYHOP;
-	if (ip6_parse_tlv(tlvprochopopt_lst, skb,
+	if (ip6_parse_tlv(IPV6_TLV_CLASS_FLAG_HOPOPT, skb,
 			  init_net.ipv6.sysctl.max_hbh_opts_cnt)) {
 		skb->transport_header += extlen;
 		opt = IP6CB(skb);
diff --git a/net/ipv6/exthdrs_core.c b/net/ipv6/exthdrs_core.c
index e875f1a..aaa4582 100644
--- a/net/ipv6/exthdrs_core.c
+++ b/net/ipv6/exthdrs_core.c
@@ -418,3 +418,282 @@ struct ipv6_txoptions *ipv6_fixup_options(struct ipv6_txoptions *opt_space,
 	return opt;
 }
 EXPORT_SYMBOL_GPL(ipv6_fixup_options);
+
+/* TLV parameter table functions and structures */
+
+static void tlv_param_table_release(struct rcu_head *rcu)
+{
+	struct tlv_param_table_data *tpt =
+	    container_of(rcu, struct tlv_param_table_data, rcu);
+
+	vfree(tpt);
+}
+
+/* Default (unset) values for TLV parameters */
+static const struct tlv_proc tlv_default_proc = {
+	.params.rx_class = 0,
+};
+
+static size_t tlv_param_table_size(unsigned char count)
+{
+	return sizeof(struct tlv_param_table_data) +
+	    (count * sizeof(struct tlv_proc));
+}
+
+static DEFINE_MUTEX(tlv_mutex);
+
+/* mutex held */
+static int __tlv_set_proc(struct tlv_param_table *tlv_param_table,
+			  unsigned char type, const struct tlv_ops *ops,
+			  const struct tlv_params *params)
+{
+	struct tlv_param_table_data *tpt, *old;
+	struct tlv_proc *tproc;
+	unsigned char count, pos;
+
+	old = rcu_dereference_protected(tlv_param_table->data,
+					lockdep_is_held(&tlv_mutex));
+
+	if (old->entries[type]) {
+		/* Type is already set, modifying entry */
+		pos = old->entries[type];
+		count = old->count;
+
+		/* If ops is not provided, take them from existing proc */
+		if (!ops)
+			ops = &old->procs[pos].ops;
+	} else {
+		/* Type entry unset, need to create new entry */
+		pos = old->count;
+		count = pos + 1;
+	}
+
+	tpt = vmalloc(tlv_param_table_size(count));
+	if (!tpt)
+		return -ENOMEM;
+
+	memcpy(tpt, old, tlv_param_table_size(old->count));
+
+	tproc = &tpt->procs[pos];
+	tproc->params = *params;
+	tproc->ops = ops ? *ops : tlv_default_proc.ops;
+
+	tpt->entries[type] = pos;
+	tpt->count = count;
+
+	rcu_assign_pointer(tlv_param_table->data, tpt);
+
+	call_rcu(&old->rcu, tlv_param_table_release);
+
+	return 0;
+}
+
+/* mutex held */
+static int __tlv_unset_proc(struct tlv_param_table *tlv_param_table,
+			    unsigned char type)
+{
+	struct tlv_param_table_data *tpt, *old;
+	unsigned char pos;
+	int i;
+
+	old = rcu_dereference_protected(tlv_param_table->data,
+					lockdep_is_held(&tlv_mutex));
+
+	if (!old->entries[type]) {
+		/* Type entry already unset, nothing to do */
+		return 0;
+	}
+
+	tpt = vmalloc(tlv_param_table_size(old->count - 1));
+	if (!tpt)
+		return -ENOMEM;
+
+	pos = old->entries[type];
+
+	memcpy(tpt->procs, old->procs, pos * sizeof(struct tlv_proc));
+	memcpy(&tpt->procs[pos], &old->procs[pos + 1],
+	       (old->count - pos - 1) * sizeof(struct tlv_proc));
+
+	for (i = 0; i < 256; i++) {
+		if (old->entries[i] > pos)
+			tpt->entries[i] = old->entries[i] - 1;
+		else
+			tpt->entries[i] = old->entries[i];
+	}
+
+	/* Clear entry for type being unset (point to default params) */
+	tpt->entries[type] = 0;
+
+	tpt->count = old->count - 1;
+
+	rcu_assign_pointer(tlv_param_table->data, tpt);
+
+	call_rcu(&old->rcu, tlv_param_table_release);
+
+	return 0;
+}
+
+static void __tlv_destroy_param_table(struct tlv_param_table *tlv_param_table)
+{
+	struct tlv_param_table_data *tpt;
+
+	mutex_lock(&tlv_mutex);
+
+	tpt = rcu_dereference_protected(tlv_param_table->data,
+					lockdep_is_held(&tlv_mutex));
+	if (tpt) {
+		rcu_assign_pointer(tlv_param_table->data, NULL);
+		call_rcu(&tpt->rcu, tlv_param_table_release);
+	}
+
+	mutex_unlock(&tlv_mutex);
+}
+
+int tlv_set_proc(struct tlv_param_table *tlv_param_table, unsigned char type,
+		 const struct tlv_proc *proc)
+{
+	int ret;
+
+	if (type < 2)
+		return -EINVAL;
+
+	mutex_lock(&tlv_mutex);
+	ret = __tlv_set_proc(tlv_param_table, type, &proc->ops,
+			     &proc->params);
+	mutex_unlock(&tlv_mutex);
+
+	return ret;
+}
+EXPORT_SYMBOL(tlv_set_proc);
+
+int tlv_unset_proc(struct tlv_param_table *tlv_param_table,
+		   unsigned char type)
+{
+	int ret;
+
+	if (type < 2)
+		return -EINVAL;
+
+	mutex_lock(&tlv_mutex);
+	ret = __tlv_unset_proc(tlv_param_table, type);
+	mutex_unlock(&tlv_mutex);
+
+	return ret;
+}
+EXPORT_SYMBOL(tlv_unset_proc);
+
+int tlv_set_params(struct tlv_param_table *tlv_param_table,
+		   unsigned char type, const struct tlv_params *params)
+{
+	int ret;
+
+	if (type < 2)
+		return -EINVAL;
+
+	mutex_lock(&tlv_mutex);
+	ret = __tlv_set_proc(tlv_param_table, type, NULL, params);
+	mutex_unlock(&tlv_mutex);
+
+	return ret;
+}
+EXPORT_SYMBOL(tlv_set_params);
+
+/* tlv_internal_proc_type is used to check it the TLV proc was set
+ * internally. This is deduced by checking if any operations are
+ * defined.
+ */
+static bool tlv_internal_proc_type(struct tlv_proc *proc)
+{
+	return !!proc->ops.func;
+}
+
+int tlv_unset_params(struct tlv_param_table *tlv_param_table,
+		     unsigned char type)
+{
+	struct tlv_param_table_data *tpt;
+	struct tlv_proc *proc;
+	int entry, ret = 0;
+
+	mutex_lock(&tlv_mutex);
+
+	tpt = rcu_dereference_protected(tlv_param_table->data,
+					lockdep_is_held(&tlv_mutex));
+	if (!tpt)
+		goto out;
+
+	entry = tpt->entries[type];
+	if (!entry) {
+		/* Entry is already unset */
+		goto out;
+	}
+
+	proc = &tpt->procs[entry];
+
+	if (tlv_internal_proc_type(proc)) {
+		/* TLV was set by internal source, so maintain
+		 * the non-parameter fields (i.e. the operations).
+		 */
+		ret = __tlv_set_proc(tlv_param_table, type, &proc->ops,
+				     &tlv_default_proc.params);
+	} else {
+		ret = __tlv_unset_proc(tlv_param_table, type);
+	}
+
+out:
+	mutex_unlock(&tlv_mutex);
+
+	return ret;
+}
+EXPORT_SYMBOL(tlv_unset_params);
+
+int exthdrs_init(struct tlv_param_table *tlv_param_table,
+		 const struct tlv_proc_init *tlv_init_params,
+		 int num_init_params)
+{
+	struct tlv_param_table_data *tpt;
+	size_t tsize;
+	int i;
+
+	tsize = tlv_param_table_size(num_init_params + 1);
+
+	tpt = vmalloc(tsize);
+	if (!tpt)
+		return -ENOMEM;
+
+	memset(tpt, 0, tsize);
+
+	/* Zeroth TLV proc entry is default */
+	tpt->procs[0] = tlv_default_proc;
+
+	for (i = 0; i < num_init_params; i++) {
+		const struct tlv_proc_init *tpi = &tlv_init_params[i];
+		struct tlv_proc *tp = &tpt->procs[i + 1];
+
+		if (WARN_ON(tpi->type < 2)) {
+			 /* Padding TLV initialized? */
+			vfree(tpt);
+			return -EINVAL;
+		}
+		if (WARN_ON(tpt->entries[tpi->type])) {
+			/* TLV type already set */
+			vfree(tpt);
+			return -EINVAL;
+		}
+
+		*tp = tpi->proc;
+		tpt->entries[tpi->type] = i + 1;
+	}
+
+	tpt->count = num_init_params + 1;
+
+	RCU_INIT_POINTER(tlv_param_table->data, tpt);
+
+	return 0;
+}
+EXPORT_SYMBOL(exthdrs_init);
+
+void exthdrs_fini(struct tlv_param_table *tlv_param_table)
+{
+	__tlv_destroy_param_table(tlv_param_table);
+}
+EXPORT_SYMBOL(exthdrs_fini);
diff --git a/net/ipv6/exthdrs_options.c b/net/ipv6/exthdrs_options.c
index 032e072..9be82ef 100644
--- a/net/ipv6/exthdrs_options.c
+++ b/net/ipv6/exthdrs_options.c
@@ -15,7 +15,7 @@
 /* Destination options header */
 
 #if IS_ENABLED(CONFIG_IPV6_MIP6)
-static bool ipv6_dest_hao(struct sk_buff *skb, int optoff)
+static bool ipv6_dest_hao(unsigned int class, struct sk_buff *skb, int optoff)
 {
 	struct ipv6_destopt_hao *hao;
 	struct inet6_skb_parm *opt = IP6CB(skb);
@@ -74,16 +74,6 @@ static bool ipv6_dest_hao(struct sk_buff *skb, int optoff)
 }
 #endif
 
-const struct tlvtype_proc tlvprocdestopt_lst[] = {
-#if IS_ENABLED(CONFIG_IPV6_MIP6)
-	{
-		.type	= IPV6_TLV_HAO,
-		.func	= ipv6_dest_hao,
-	},
-#endif
-	{-1,			NULL}
-};
-
 /* Hop-by-hop options */
 
 /* Note: we cannot rely on skb_dst(skb) before we assign it in
@@ -102,7 +92,7 @@ static inline struct net *ipv6_skb_net(struct sk_buff *skb)
 
 /* Router Alert as of RFC 2711 */
 
-static bool ipv6_hop_ra(struct sk_buff *skb, int optoff)
+static bool ipv6_hop_ra(unsigned int class, struct sk_buff *skb, int optoff)
 {
 	const unsigned char *nh = skb_network_header(skb);
 
@@ -120,7 +110,7 @@ static bool ipv6_hop_ra(struct sk_buff *skb, int optoff)
 
 /* Jumbo payload */
 
-static bool ipv6_hop_jumbo(struct sk_buff *skb, int optoff)
+static bool ipv6_hop_jumbo(unsigned int class, struct sk_buff *skb, int optoff)
 {
 	const unsigned char *nh = skb_network_header(skb);
 	struct inet6_dev *idev = __in6_dev_get_safely(skb->dev);
@@ -164,7 +154,8 @@ static bool ipv6_hop_jumbo(struct sk_buff *skb, int optoff)
 
 /* CALIPSO RFC 5570 */
 
-static bool ipv6_hop_calipso(struct sk_buff *skb, int optoff)
+static bool ipv6_hop_calipso(unsigned int class, struct sk_buff *skb,
+			     int optoff)
 {
 	const unsigned char *nh = skb_network_header(skb);
 
@@ -184,18 +175,44 @@ static bool ipv6_hop_calipso(struct sk_buff *skb, int optoff)
 	return false;
 }
 
-const struct tlvtype_proc tlvprochopopt_lst[] = {
+static const struct tlv_proc_init tlv_init_params[] __initconst = {
 	{
-		.type	= IPV6_TLV_ROUTERALERT,
-		.func	= ipv6_hop_ra,
+		.type = IPV6_TLV_HAO,
+
+		.proc.ops.func = ipv6_dest_hao,
+		.proc.params.rx_class = IPV6_TLV_CLASS_FLAG_DSTOPT,
 	},
 	{
-		.type	= IPV6_TLV_JUMBO,
-		.func	= ipv6_hop_jumbo,
+		.type = IPV6_TLV_ROUTERALERT,
+
+		.proc.ops.func = ipv6_hop_ra,
+		.proc.params.rx_class = IPV6_TLV_CLASS_FLAG_HOPOPT,
 	},
 	{
-		.type	= IPV6_TLV_CALIPSO,
-		.func	= ipv6_hop_calipso,
+		.type = IPV6_TLV_JUMBO,
+
+		.proc.ops.func	= ipv6_hop_jumbo,
+		.proc.params.rx_class = IPV6_TLV_CLASS_FLAG_HOPOPT,
+	},
+	{
+		.type = IPV6_TLV_CALIPSO,
+
+		.proc.ops.func = ipv6_hop_calipso,
+		.proc.params.rx_class = IPV6_TLV_CLASS_FLAG_HOPOPT,
 	},
-	{ -1, }
 };
+
+struct tlv_param_table __rcu ipv6_tlv_param_table;
+
+static int __init ipv6_exthdrs_init(void)
+{
+	return exthdrs_init(&ipv6_tlv_param_table, tlv_init_params,
+			    ARRAY_SIZE(tlv_init_params));
+}
+module_init(ipv6_exthdrs_init);
+
+static void __exit ipv6_exthdrs_fini(void)
+{
+	exthdrs_fini(&ipv6_tlv_param_table);
+}
+module_exit(ipv6_exthdrs_fini);
