This commit adds enough logic to translate the initial table, as provided by ip6table modules, to an xt2 table. The translation code goes into a file xt1_compat.c that is planned to be shared with the other *_tables too, hence the little #define/#include hack. I deem this is the better evil -- at least when done right, and it looks promising -- compared to the real code duplication that is currently in place. The "filter2" table can be used for testing: modprobing it/rmmod, and that packets flow through xt2_do_table(). /usr/sbin/ip6tables cannot interact with it yet as of this commit. Signed-off-by: Jan Engelhardt <jengelh@xxxxxxxxxx> --- include/linux/netfilter/x_tables.h | 9 + include/linux/netfilter_ipv6/ip6_tables.h | 3 + include/net/netns/x_tables.h | 2 +- net/ipv6/netfilter/Kconfig | 1 + net/ipv6/netfilter/ip6_tables.c | 30 ++-- net/ipv6/netfilter/ip6table_filter2.c | 21 ++- net/netfilter/Kconfig | 6 + net/netfilter/Makefile | 1 + net/netfilter/x_tables.c | 3 +- net/netfilter/xt1_postshared.c | 51 +++++ net/netfilter/xt1_support.c | 37 ++++ net/netfilter/xt1_translat.c | 292 +++++++++++++++++++++++++++++ 12 files changed, 432 insertions(+), 24 deletions(-) create mode 100644 net/netfilter/xt1_postshared.c create mode 100644 net/netfilter/xt1_support.c create mode 100644 net/netfilter/xt1_translat.c diff --git a/include/linux/netfilter/x_tables.h b/include/linux/netfilter/x_tables.h index 0608e64..cf8e65e 100644 --- a/include/linux/netfilter/x_tables.h +++ b/include/linux/netfilter/x_tables.h @@ -482,6 +482,8 @@ struct xt2_entry_target { * @table: back link to table chain is contained in * @comefrom: bitmask from which hooks the chain is entered * (currently needed for xt_check_*) + * @xt1_offset: temporary field to hold the chain offset + * used during xt2->xt1 conversion (xt1_compat.c) */ struct xt2_chain { struct list_head anchor; @@ -489,6 +491,7 @@ struct xt2_chain { char name[31]; struct xt2_table *table; unsigned int comefrom; + unsigned int xt1_offset; }; /** @@ -508,6 +511,7 @@ enum { * @chain_list: list of chains (struct xt2_chain) * @name: name of this table * @nfproto: nfproto the table is used exclusively with + * @valid_hooks: hooks the table can be entered on * @rq_stacksize: Size of the jumpstack. This is usually set to the * number of user chains -- since tables cannot have * loops, at most that many jumps can possibly be made -- @@ -524,6 +528,7 @@ struct xt2_table { struct list_head chain_list; char name[11]; uint8_t nfproto; + unsigned int valid_hooks; unsigned int rq_stacksize, stacksize; unsigned int *stackptr; @@ -686,11 +691,15 @@ extern struct nf_hook_ops *xt_hook_link(const struct xt_table *, nf_hookfn *); extern void xt_hook_unlink(const struct xt_table *, struct nf_hook_ops *); extern void *xt_repldata_create(const struct xt_table *); +extern struct xt2_chain *xts_lookup_chain(const struct xt2_table *, + unsigned int); + extern struct xt2_rule *xt2_rule_new(struct xt2_chain *); extern int xt2_rule_add_match(struct xt2_rule *, const char *, uint8_t, const void *, unsigned int, bool); extern int xt2_rule_add_target(struct xt2_rule *, const char *, uint8_t, const void *, unsigned int, bool); +extern void xt2_rule_free(struct xt2_rule *); extern struct xt2_chain *xt2_chain_new(struct xt2_table *, const char *); extern void xt2_chain_append(struct xt2_rule *); diff --git a/include/linux/netfilter_ipv6/ip6_tables.h b/include/linux/netfilter_ipv6/ip6_tables.h index 89e0ab1..421c2d5 100644 --- a/include/linux/netfilter_ipv6/ip6_tables.h +++ b/include/linux/netfilter_ipv6/ip6_tables.h @@ -317,6 +317,9 @@ extern unsigned int ip6t_do_table(struct sk_buff *skb, const struct net_device *out, struct xt_table *table); +extern struct xt2_table *ip6t2_register_table(struct net *, + const struct xt_table *, const struct ip6t_replace *); + /* Check for an extension */ extern int ip6t_ext_hdr(u8 nexthdr); /* find specified header and get offset to it */ diff --git a/include/net/netns/x_tables.h b/include/net/netns/x_tables.h index c4abab8..a63aa04 100644 --- a/include/net/netns/x_tables.h +++ b/include/net/netns/x_tables.h @@ -16,7 +16,7 @@ struct netns_xt { struct netns_xt2 { struct mutex table_lock; struct list_head table_list[NFPROTO_NUMPROTO]; - struct xt_table *ipv6_filter; + struct xt2_table_link *ipv6_filter; }; #endif diff --git a/net/ipv6/netfilter/Kconfig b/net/ipv6/netfilter/Kconfig index 29d643b..46163b1 100644 --- a/net/ipv6/netfilter/Kconfig +++ b/net/ipv6/netfilter/Kconfig @@ -46,6 +46,7 @@ config IP6_NF_IPTABLES tristate "IP6 tables support (required for filtering)" depends on INET && IPV6 select NETFILTER_XTABLES + select NETFILTER_XT1_SUPPORT default m if NETFILTER_ADVANCED=n help ip6tables is a general, extensible packet identification framework. diff --git a/net/ipv6/netfilter/ip6_tables.c b/net/ipv6/netfilter/ip6_tables.c index 7eb9a57..edb00ad 100644 --- a/net/ipv6/netfilter/ip6_tables.c +++ b/net/ipv6/netfilter/ip6_tables.c @@ -67,6 +67,19 @@ do { \ #define inline #endif +static int mark_source_chains(const struct xt_table_info *, + unsigned int, void *); + +#define xtsub_entry ip6t_entry +#define xtsub_replace ip6t_replace +#define xtsub_error_target ip6t_error_target +#define XTSUB_NFPROTO_IPV6 1 +#define XTSUB(x) ip6t_ ## x +#define XTSUB2(x) ip6t2_ ## x + +#include "../../netfilter/xt1_translat.c" +#include "../../netfilter/xt1_postshared.c" + /* We keep a set of rules for each CPU, so we can avoid write-locking them in the softirq when updating the counters and therefore @@ -807,21 +820,6 @@ find_check_entry(struct ip6t_entry *e, const char *name, unsigned int size) return ret; } -static bool check_underflow(const struct ip6t_entry *e) -{ - const struct ip6t_entry_target *t; - unsigned int verdict; - - if (!unconditional(&e->ipv6)) - return false; - t = ip6t_get_target_c(e); - if (strcmp(t->u.user.name, XT_STANDARD_TARGET) != 0) - return false; - verdict = ((struct ip6t_standard_target *)t)->verdict; - verdict = -verdict - 1; - return verdict == NF_DROP || verdict == NF_ACCEPT; -} - static int check_entry_size_and_hooks(struct ip6t_entry *e, struct xt_table_info *newinfo, @@ -853,7 +851,7 @@ check_entry_size_and_hooks(struct ip6t_entry *e, if ((unsigned char *)e - base == hook_entries[h]) newinfo->hook_entry[h] = hook_entries[h]; if ((unsigned char *)e - base == underflows[h]) { - if (!check_underflow(e)) { + if (!ip6t2_check_underflow(e)) { pr_err("Underflows must be unconditional and " "use the STANDARD target with " "ACCEPT/DROP\n"); diff --git a/net/ipv6/netfilter/ip6table_filter2.c b/net/ipv6/netfilter/ip6table_filter2.c index 790552b..7456686 100644 --- a/net/ipv6/netfilter/ip6table_filter2.c +++ b/net/ipv6/netfilter/ip6table_filter2.c @@ -38,7 +38,14 @@ ip6table_filter_hook(unsigned int hook, int (*okfn)(struct sk_buff *)) { const struct net *net = dev_net((in != NULL) ? in : out); - return ip6t_do_table(skb, hook, in, out, net->ipv6.ip6table_filter); + const struct xt2_table_link *link; + unsigned int verdict; + + rcu_read_lock(); + link = rcu_dereference(net->xt2.ipv6_filter); + verdict = xt2_do_table(skb, hook, in, out, link->table); + rcu_read_unlock(); + return verdict; } /* Default to forward because I got too much mail already. */ @@ -48,6 +55,7 @@ module_param(forward, bool, 0000); static int __net_init ip6table_filter_net_init(struct net *net) { struct ip6t_replace *repl = xt_repldata_create(&packet_filter); + struct xt2_table *table; if (repl == NULL) return -ENOMEM; @@ -55,17 +63,18 @@ static int __net_init ip6table_filter_net_init(struct net *net) ((struct ip6t_standard *)repl->entries)[1].target.verdict = -forward - 1; - net->xt2.ipv6_filter = - ip6t_register_table(net, &packet_filter, repl); + table = ip6t2_register_table(net, &packet_filter, repl); kfree(repl); - if (IS_ERR(net->xt2.ipv6_filter)) - return PTR_ERR(net->xt2.ipv6_filter); + if (IS_ERR(table)) + return PTR_ERR(table); + net->xt2.ipv6_filter = xt2_tlink_lookup(net, table->name, + table->nfproto, XT2_NO_RCULOCK); return 0; } static void __net_exit ip6table_filter_net_exit(struct net *net) { - ip6t_unregister_table(net->xt2.ipv6_filter); + xt2_table_destroy(net, net->xt2.ipv6_filter->table); } static struct pernet_operations ip6table_filter_net_ops = { diff --git a/net/netfilter/Kconfig b/net/netfilter/Kconfig index 773c360..0f18528 100644 --- a/net/netfilter/Kconfig +++ b/net/netfilter/Kconfig @@ -301,6 +301,12 @@ config NETFILTER_XTABLES if NETFILTER_XTABLES +config NETFILTER_XT1_SUPPORT + tristate + select NETFILTER_XT_MATCH_QUOTA + ---help--- + Protocol-agnostic part of the xt1 <-> xt2 translation layer. + # alphabetically ordered list of targets config NETFILTER_XT_TARGET_CLASSIFY diff --git a/net/netfilter/Makefile b/net/netfilter/Makefile index 49f62ee..fead6b4 100644 --- a/net/netfilter/Makefile +++ b/net/netfilter/Makefile @@ -39,6 +39,7 @@ obj-$(CONFIG_NETFILTER_TPROXY) += nf_tproxy_core.o # generic X tables obj-$(CONFIG_NETFILTER_XTABLES) += x_tables.o xt_tcpudp.o +obj-$(CONFIG_NETFILTER_XT1_SUPPORT) += xt1_support.o # targets obj-$(CONFIG_NETFILTER_XT_TARGET_CLASSIFY) += xt_CLASSIFY.o diff --git a/net/netfilter/x_tables.c b/net/netfilter/x_tables.c index 105039a..f8ff821 100644 --- a/net/netfilter/x_tables.c +++ b/net/netfilter/x_tables.c @@ -1476,7 +1476,7 @@ int xt2_rule_add_target(struct xt2_rule *rule, const char *ext_name, } EXPORT_SYMBOL_GPL(xt2_rule_add_target); -static void xt2_rule_free(struct xt2_rule *rule) +void xt2_rule_free(struct xt2_rule *rule) { struct xt2_entry_target *etarget, *next_etarget; struct xt2_entry_match *ematch, *next_ematch; @@ -1515,6 +1515,7 @@ static void xt2_rule_free(struct xt2_rule *rule) } kfree(rule); } +EXPORT_SYMBOL_GPL(xt2_rule_free); struct xt2_chain *xt2_chain_new(struct xt2_table *table, const char *name) { diff --git a/net/netfilter/xt1_postshared.c b/net/netfilter/xt1_postshared.c new file mode 100644 index 0000000..9dcbac1 --- /dev/null +++ b/net/netfilter/xt1_postshared.c @@ -0,0 +1,51 @@ +/* + * xt1 <-> xt2 translation layer, per-nfproto specific part + * Copyright © Jan Engelhardt, 2009 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + */ +#include <linux/kernel.h> +#include <linux/netfilter.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/netfilter_ipv6/ip6_tables.h> + +struct xt2_table * +XTSUB2(register_table)(struct net *net, const struct xt_table *classic_table, + const struct xtsub_replace *repl) +{ + struct xt2_table *table; + void *blob; + int ret; + + table = xt2_table_new(); + if (table == NULL) + return ERR_PTR(-ENOMEM); + /* Need a writable copy for check_entry_and_size_hooks. */ + ret = -ENOMEM; + blob = vmalloc(repl->size); + if (blob == NULL) + goto out; + memcpy(blob, repl->entries, repl->size); + strncpy(table->name, classic_table->name, sizeof(table->name)); + table->name[sizeof(table->name)-1] = '\0'; + table->valid_hooks = classic_table->valid_hooks; + table->nfproto = classic_table->af; + + ret = XTSUB2(table_to_xt2)(table, blob, repl); + vfree(blob); + if (ret < 0) + goto out; + ret = xt2_table_register(net, table); + if (ret < 0) + goto out; + return table; + + out: + xt2_table_destroy(NULL, table); + return ERR_PTR(ret); +} +EXPORT_SYMBOL_GPL(XTSUB2(register_table)); diff --git a/net/netfilter/xt1_support.c b/net/netfilter/xt1_support.c new file mode 100644 index 0000000..d15bfb7 --- /dev/null +++ b/net/netfilter/xt1_support.c @@ -0,0 +1,37 @@ +/* + * xt1 <-> xt2 translation layer, protocol independent parts + * Copyright © Jan Engelhardt, 2009 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + */ +#include <linux/module.h> +#include <linux/netfilter/x_tables.h> + +/** + * @table: table to search in + * @needle: rule offset in the xt1 blob + * + * Find the xt2 chain for a given xt1 offset. This assumes the chains are + * sorted by xt1_offset, which they should be given our linear translation + * mechanism in xt1_compat.c. + */ +struct xt2_chain * +xts_lookup_chain(const struct xt2_table *table, unsigned int needle) +{ + struct xt2_chain *chain, *ret = NULL; + + list_for_each_entry(chain, &table->chain_list, anchor) { + if (chain->xt1_offset <= needle) + ret = chain; + else + return ret; + } + + return ret; +} +EXPORT_SYMBOL_GPL(xts_lookup_chain); + +MODULE_LICENSE("GPL"); diff --git a/net/netfilter/xt1_translat.c b/net/netfilter/xt1_translat.c new file mode 100644 index 0000000..a1663ba --- /dev/null +++ b/net/netfilter/xt1_translat.c @@ -0,0 +1,292 @@ +/* + * xt1 <-> xt2 translation layer, proto-format specific part + * Copyright © Jan Engelhardt, 2009 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + */ +#include <linux/kernel.h> +#include <linux/netfilter.h> +#include <linux/slab.h> +#include <linux/netfilter/x_tables.h> +#include <linux/netfilter/xt_quota.h> +#include <linux/netfilter_ipv6/ip6_tables.h> +#if !defined(XTSUB_NFPROTO_IPV6) +# error Need to define XTSUB_NFPROTO_xxx. +#endif + +#ifdef XTSUB_NFPROTO_IPV6 +static const struct ip6t_ip6 xtsub_uncond; + +static inline bool XTSUB2(unconditional)(const struct xtsub_entry *e) +{ + return memcmp(&e->ipv6, &xtsub_uncond, sizeof(xtsub_uncond)) == 0; +} +#endif + +static inline struct xt_entry_target * +XTSUB2(get_target)(const struct xtsub_entry *e) +{ + return (void *)e + e->target_offset; +} + +static bool XTSUB2(check_underflow)(const struct xtsub_entry *e) +{ + const struct xt_entry_target *t; + unsigned int verdict; + + if (!XTSUB2(unconditional)(e)) + return false; + t = XTSUB2(get_target)(e); + if (strcmp(t->u.user.name, XT_STANDARD_TARGET) != 0) + return false; + verdict = ((struct xt_standard_target *)t)->verdict; + verdict = -verdict - 1; + return verdict == NF_DROP || verdict == NF_ACCEPT; +} + +/** + * Check an xt1 entry for sanity and create chains as we find them. + */ +static int +XTSUB2(rule_check)(struct xtsub_entry *e, struct xt2_table *table, + const void *base, const struct xtsub_replace *repl) +{ + const void *limit = (void *)base + repl->size; + unsigned int delta = (void *)e - base; + struct xtsub_error_target *t; + struct xt2_chain *chain; + unsigned int hook; + + if ((unsigned long)e % __alignof__(struct xtsub_entry) != 0 || + (void *)e + sizeof(struct xtsub_entry) >= limit || + e->next_offset < sizeof(struct xtsub_entry) + + sizeof(struct xt_entry_target)) + return -EINVAL; + + /* Check hooks & underflows */ + for (hook = 0; hook < ARRAY_SIZE(repl->hook_entry); ++hook) { + if (!(repl->valid_hooks & (1 << hook))) + continue; + if (delta == repl->hook_entry[hook]) { + chain = xt2_chain_new(table, NULL); + if (chain == NULL) + return -ENOMEM; + + table->entrypoint[hook] = chain; + chain->comefrom = e->comefrom; + /* Aid for finding chains for a given delta. */ + chain->xt1_offset = delta; + } + if (delta == repl->underflow[hook]) + if (!XTSUB2(check_underflow)(e)) { + pr_err("Underflows must be unconditional and " + "use the STANDARD target with " + "ACCEPT/DROP\n"); + return -EINVAL; + } + } + + /* Clear counters and comefrom */ + memset(&e->counters, 0, sizeof(e->counters)); + e->comefrom = 0; + + t = (void *)XTSUB2(get_target)(e); + if (strcmp(t->target.u.user.name, XT_ERROR_TARGET) == 0) { + t->errorname[sizeof(t->errorname)-1] = '\0'; + if (strcmp(t->errorname, XT_ERROR_TARGET) == 0) + /* End of ruleset */ + return 0; + chain = xt2_chain_new(table, t->errorname); + if (chain == NULL) + return -ENOMEM; + chain->xt1_offset = delta; + /* Chain marker translated. */ + return 0; + } + + return 1; +} + +static int +XTSUB2(target_to_xt2)(struct xt2_rule *rule, const struct xtsub_entry *entry, + unsigned int entry_offset) +{ + const struct xt_entry_target *etarget = XTSUB2(get_target)(entry); + const struct xt_standard_target *st; + struct xt2_entry_target *ntarget; + + if (strcmp(etarget->u.user.name, XT_STANDARD_TARGET) != 0) + return xt2_rule_add_oldtarget(rule, etarget); + + ntarget = kmalloc(sizeof(*ntarget), GFP_KERNEL); + if (ntarget == NULL) + return -ENOMEM; + INIT_LIST_HEAD(&ntarget->anchor); + + st = (void *)etarget; + if (st->verdict == XT_RETURN) { + ntarget->ext = XT2_FINAL_VERDICT; + ntarget->verdict = XT_RETURN; + } else if (st->verdict < 0) { + ntarget->ext = XT2_FINAL_VERDICT; + ntarget->verdict = -st->verdict - 1; + } else if (st->verdict == entry_offset + entry->next_offset) { + ntarget->ext = XT2_FINAL_VERDICT; + ntarget->verdict = XT_CONTINUE; +#ifdef XTSUB_NFPROTO_IPV6 + } else if (entry->ipv6.flags & IP6T_F_GOTO) { + ntarget->ext = XT2_ACTION_GOTO; + ntarget->r_goto = xts_lookup_chain(rule->chain->table, + st->verdict); + /* debug: (we already checked loopfreeness before) */ + if (ntarget->r_goto == rule->chain) + return -ELOOP; +#endif + } else { + ntarget->ext = XT2_ACTION_JUMP; + ntarget->r_jump = xts_lookup_chain(rule->chain->table, + st->verdict); + if (ntarget->r_jump == rule->chain) + return -ELOOP; + } + + list_add_tail(&ntarget->anchor, &rule->target_list); + return 0; +} + +/** + * Translate a single ip6t_entry into a xt2 rule. + */ +static struct xt2_rule * +XTSUB2(rule_to_xt2)(struct xt2_chain *chain, const struct xtsub_entry *entry, + unsigned int entry_offset) +{ + const struct xt_entry_match *ematch; + struct xt_quota_mtinfo3 byte_counter = {.flags = XT_QUOTA_GROW}; + struct xt_quota_mtinfo3 packet_counter = + {.flags = XT_QUOTA_GROW | XT_QUOTA_PACKET}; + struct xt2_rule *rule; + int ret; + + rule = xt2_rule_new(chain); + if (rule == NULL) + return ERR_PTR(-ENOMEM); + + rule->chain->comefrom = entry->comefrom; +#ifdef XTSUB_NFPROTO_IPV6 + rule->l4proto = entry->ipv6.proto; + if (entry->ipv6.flags & IP6T_INV_PROTO) + rule->flags |= XT2_INV_L4PROTO; + ret = xt2_rule_add_match(rule, "ipv6", 0, &entry->ipv6, + sizeof(entry->ipv6), false); +#endif + if (ret < 0) + goto out; + + xt_ematch_foreach(ematch, entry) { + ret = xt2_rule_add_oldmatch(rule, ematch); + if (ret < 0) + goto out; + } + + ret = xt2_rule_add_match(rule, "quota", 3, &byte_counter, + sizeof(byte_counter), false); + if (ret < 0) + goto out; + ret = xt2_rule_add_match(rule, "quota", 3, &packet_counter, + sizeof(packet_counter), false); + if (ret < 0) + goto out; + ret = XTSUB2(target_to_xt2)(rule, entry, entry_offset); + if (ret < 0) + goto out; + return rule; + + out: + xt2_rule_free(rule); + return ERR_PTR(ret); +} + +/** + * @table: new table + * @entry0: blob of <struct ip6t_entry>s + * @repl: blob metadata + * + * Convert a blob table into a xt2 table. + */ +static int XTSUB2(table_to_xt2)(struct xt2_table *table, void *entry0, + const struct xtsub_replace *repl) +{ + struct xt_table_info mark_param; + struct xtsub_entry *iter; + unsigned int i; + int ret = 0; + + i = 0; + /* Walk through entries, checking offsets, creating chains. */ + xt_entry_foreach(iter, entry0, repl->size) { + ret = XTSUB2(rule_check)(iter, table, entry0, repl); + ++i; + if (ret < 0) + return ret; + else if (ret == 0) + continue; + } + + if (i != repl->num_entries) + return -EINVAL; + + /* Check hooks all assigned */ + for (i = 0; i < ARRAY_SIZE(repl->hook_entry); ++i) { + /* Only hooks which are valid */ + if (!(repl->valid_hooks & (1 << i))) + continue; + if (table->entrypoint[i] == NULL) + return -EINVAL; + } + + memcpy(mark_param.hook_entry, repl->hook_entry, + sizeof(repl->hook_entry)); + mark_param.size = repl->size; + if (!mark_source_chains(&mark_param, repl->valid_hooks, entry0)) + return -ELOOP; + + /* Now process rules. */ + xt_entry_foreach(iter, entry0, repl->size) { + unsigned int hook, delta = (void *)iter - (void *)entry0; + const struct xt_entry_target *t; + struct xt2_chain *chain; + struct xt2_rule *rule; + + t = XTSUB2(get_target)(iter); + if (strcmp(t->u.user.name, XT_ERROR_TARGET) == 0) + /* already translated */ + continue; + + /* + * Do not ignore base chain policies (which are rules), though. + * We need these for the counters. + */ + chain = xts_lookup_chain(table, delta); + if (chain == NULL) + return -EINVAL; + rule = XTSUB2(rule_to_xt2)(chain, iter, delta); + if (IS_ERR(rule)) + return PTR_ERR(rule); + xt2_chain_append(rule); + + for (hook = 0; hook < ARRAY_SIZE(repl->underflow); ++hook) { + if (!(table->valid_hooks & (1 << hook))) + continue; + if (delta == repl->underflow[hook]) { + table->underflow[hook] = rule; + break; + } + } + } + + return 0; +} -- 1.6.3.3 -- To unsubscribe from this list: send the line "unsubscribe netfilter-devel" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html