| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * xt_HMARK - Netfilter module to set mark by means of hashing |
| * |
| * (C) 2012 by Hans Schillstrom <hans.schillstrom@ericsson.com> |
| * (C) 2012 by Pablo Neira Ayuso <pablo@netfilter.org> |
| */ |
| |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
| |
| #include <linux/module.h> |
| #include <linux/skbuff.h> |
| #include <linux/icmp.h> |
| |
| #include <linux/netfilter/x_tables.h> |
| #include <linux/netfilter/xt_HMARK.h> |
| |
| #include <net/ip.h> |
| #if IS_ENABLED(CONFIG_NF_CONNTRACK) |
| #include <net/netfilter/nf_conntrack.h> |
| #endif |
| #if IS_ENABLED(CONFIG_IP6_NF_IPTABLES) |
| #include <net/ipv6.h> |
| #include <linux/netfilter_ipv6/ip6_tables.h> |
| #endif |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Hans Schillstrom <hans.schillstrom@ericsson.com>"); |
| MODULE_DESCRIPTION("Xtables: packet marking using hash calculation"); |
| MODULE_ALIAS("ipt_HMARK"); |
| MODULE_ALIAS("ip6t_HMARK"); |
| |
| struct hmark_tuple { |
| __be32 src; |
| __be32 dst; |
| union hmark_ports uports; |
| u8 proto; |
| }; |
| |
| static inline __be32 hmark_addr6_mask(const __be32 *addr32, const __be32 *mask) |
| { |
| return (addr32[0] & mask[0]) ^ |
| (addr32[1] & mask[1]) ^ |
| (addr32[2] & mask[2]) ^ |
| (addr32[3] & mask[3]); |
| } |
| |
| static inline __be32 |
| hmark_addr_mask(int l3num, const __be32 *addr32, const __be32 *mask) |
| { |
| switch (l3num) { |
| case AF_INET: |
| return *addr32 & *mask; |
| case AF_INET6: |
| return hmark_addr6_mask(addr32, mask); |
| } |
| return 0; |
| } |
| |
| static inline void hmark_swap_ports(union hmark_ports *uports, |
| const struct xt_hmark_info *info) |
| { |
| union hmark_ports hp; |
| u16 src, dst; |
| |
| hp.b32 = (uports->b32 & info->port_mask.b32) | info->port_set.b32; |
| src = ntohs(hp.b16.src); |
| dst = ntohs(hp.b16.dst); |
| |
| if (dst > src) |
| uports->v32 = (dst << 16) | src; |
| else |
| uports->v32 = (src << 16) | dst; |
| } |
| |
| static int |
| hmark_ct_set_htuple(const struct sk_buff *skb, struct hmark_tuple *t, |
| const struct xt_hmark_info *info) |
| { |
| #if IS_ENABLED(CONFIG_NF_CONNTRACK) |
| enum ip_conntrack_info ctinfo; |
| struct nf_conn *ct = nf_ct_get(skb, &ctinfo); |
| struct nf_conntrack_tuple *otuple; |
| struct nf_conntrack_tuple *rtuple; |
| |
| if (ct == NULL) |
| return -1; |
| |
| otuple = &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple; |
| rtuple = &ct->tuplehash[IP_CT_DIR_REPLY].tuple; |
| |
| t->src = hmark_addr_mask(otuple->src.l3num, otuple->src.u3.ip6, |
| info->src_mask.ip6); |
| t->dst = hmark_addr_mask(otuple->src.l3num, rtuple->src.u3.ip6, |
| info->dst_mask.ip6); |
| |
| if (info->flags & XT_HMARK_FLAG(XT_HMARK_METHOD_L3)) |
| return 0; |
| |
| t->proto = nf_ct_protonum(ct); |
| if (t->proto != IPPROTO_ICMP) { |
| t->uports.b16.src = otuple->src.u.all; |
| t->uports.b16.dst = rtuple->src.u.all; |
| hmark_swap_ports(&t->uports, info); |
| } |
| |
| return 0; |
| #else |
| return -1; |
| #endif |
| } |
| |
| /* This hash function is endian independent, to ensure consistent hashing if |
| * the cluster is composed of big and little endian systems. */ |
| static inline u32 |
| hmark_hash(struct hmark_tuple *t, const struct xt_hmark_info *info) |
| { |
| u32 hash; |
| u32 src = ntohl(t->src); |
| u32 dst = ntohl(t->dst); |
| |
| if (dst < src) |
| swap(src, dst); |
| |
| hash = jhash_3words(src, dst, t->uports.v32, info->hashrnd); |
| hash = hash ^ (t->proto & info->proto_mask); |
| |
| return reciprocal_scale(hash, info->hmodulus) + info->hoffset; |
| } |
| |
| static void |
| hmark_set_tuple_ports(const struct sk_buff *skb, unsigned int nhoff, |
| struct hmark_tuple *t, const struct xt_hmark_info *info) |
| { |
| int protoff; |
| |
| protoff = proto_ports_offset(t->proto); |
| if (protoff < 0) |
| return; |
| |
| nhoff += protoff; |
| if (skb_copy_bits(skb, nhoff, &t->uports, sizeof(t->uports)) < 0) |
| return; |
| |
| hmark_swap_ports(&t->uports, info); |
| } |
| |
| #if IS_ENABLED(CONFIG_IP6_NF_IPTABLES) |
| static int get_inner6_hdr(const struct sk_buff *skb, int *offset) |
| { |
| struct icmp6hdr *icmp6h, _ih6; |
| |
| icmp6h = skb_header_pointer(skb, *offset, sizeof(_ih6), &_ih6); |
| if (icmp6h == NULL) |
| return 0; |
| |
| if (icmp6h->icmp6_type && icmp6h->icmp6_type < 128) { |
| *offset += sizeof(struct icmp6hdr); |
| return 1; |
| } |
| return 0; |
| } |
| |
| static int |
| hmark_pkt_set_htuple_ipv6(const struct sk_buff *skb, struct hmark_tuple *t, |
| const struct xt_hmark_info *info) |
| { |
| struct ipv6hdr *ip6, _ip6; |
| int flag = IP6_FH_F_AUTH; |
| unsigned int nhoff = 0; |
| u16 fragoff = 0; |
| int nexthdr; |
| |
| ip6 = (struct ipv6hdr *) (skb->data + skb_network_offset(skb)); |
| nexthdr = ipv6_find_hdr(skb, &nhoff, -1, &fragoff, &flag); |
| if (nexthdr < 0) |
| return 0; |
| /* No need to check for icmp errors on fragments */ |
| if ((flag & IP6_FH_F_FRAG) || (nexthdr != IPPROTO_ICMPV6)) |
| goto noicmp; |
| /* Use inner header in case of ICMP errors */ |
| if (get_inner6_hdr(skb, &nhoff)) { |
| ip6 = skb_header_pointer(skb, nhoff, sizeof(_ip6), &_ip6); |
| if (ip6 == NULL) |
| return -1; |
| /* If AH present, use SPI like in ESP. */ |
| flag = IP6_FH_F_AUTH; |
| nexthdr = ipv6_find_hdr(skb, &nhoff, -1, &fragoff, &flag); |
| if (nexthdr < 0) |
| return -1; |
| } |
| noicmp: |
| t->src = hmark_addr6_mask(ip6->saddr.s6_addr32, info->src_mask.ip6); |
| t->dst = hmark_addr6_mask(ip6->daddr.s6_addr32, info->dst_mask.ip6); |
| |
| if (info->flags & XT_HMARK_FLAG(XT_HMARK_METHOD_L3)) |
| return 0; |
| |
| t->proto = nexthdr; |
| if (t->proto == IPPROTO_ICMPV6) |
| return 0; |
| |
| if (flag & IP6_FH_F_FRAG) |
| return 0; |
| |
| hmark_set_tuple_ports(skb, nhoff, t, info); |
| return 0; |
| } |
| |
| static unsigned int |
| hmark_tg_v6(struct sk_buff *skb, const struct xt_action_param *par) |
| { |
| const struct xt_hmark_info *info = par->targinfo; |
| struct hmark_tuple t; |
| |
| memset(&t, 0, sizeof(struct hmark_tuple)); |
| |
| if (info->flags & XT_HMARK_FLAG(XT_HMARK_CT)) { |
| if (hmark_ct_set_htuple(skb, &t, info) < 0) |
| return XT_CONTINUE; |
| } else { |
| if (hmark_pkt_set_htuple_ipv6(skb, &t, info) < 0) |
| return XT_CONTINUE; |
| } |
| |
| skb->mark = hmark_hash(&t, info); |
| return XT_CONTINUE; |
| } |
| #endif |
| |
| static int get_inner_hdr(const struct sk_buff *skb, int iphsz, int *nhoff) |
| { |
| const struct icmphdr *icmph; |
| struct icmphdr _ih; |
| |
| /* Not enough header? */ |
| icmph = skb_header_pointer(skb, *nhoff + iphsz, sizeof(_ih), &_ih); |
| if (icmph == NULL || icmph->type > NR_ICMP_TYPES) |
| return 0; |
| |
| /* Error message? */ |
| if (!icmp_is_err(icmph->type)) |
| return 0; |
| |
| *nhoff += iphsz + sizeof(_ih); |
| return 1; |
| } |
| |
| static int |
| hmark_pkt_set_htuple_ipv4(const struct sk_buff *skb, struct hmark_tuple *t, |
| const struct xt_hmark_info *info) |
| { |
| struct iphdr *ip, _ip; |
| int nhoff = skb_network_offset(skb); |
| |
| ip = (struct iphdr *) (skb->data + nhoff); |
| if (ip->protocol == IPPROTO_ICMP) { |
| /* Use inner header in case of ICMP errors */ |
| if (get_inner_hdr(skb, ip->ihl * 4, &nhoff)) { |
| ip = skb_header_pointer(skb, nhoff, sizeof(_ip), &_ip); |
| if (ip == NULL) |
| return -1; |
| } |
| } |
| |
| t->src = ip->saddr & info->src_mask.ip; |
| t->dst = ip->daddr & info->dst_mask.ip; |
| |
| if (info->flags & XT_HMARK_FLAG(XT_HMARK_METHOD_L3)) |
| return 0; |
| |
| t->proto = ip->protocol; |
| |
| /* ICMP has no ports, skip */ |
| if (t->proto == IPPROTO_ICMP) |
| return 0; |
| |
| /* follow-up fragments don't contain ports, skip all fragments */ |
| if (ip_is_fragment(ip)) |
| return 0; |
| |
| hmark_set_tuple_ports(skb, (ip->ihl * 4) + nhoff, t, info); |
| |
| return 0; |
| } |
| |
| static unsigned int |
| hmark_tg_v4(struct sk_buff *skb, const struct xt_action_param *par) |
| { |
| const struct xt_hmark_info *info = par->targinfo; |
| struct hmark_tuple t; |
| |
| memset(&t, 0, sizeof(struct hmark_tuple)); |
| |
| if (info->flags & XT_HMARK_FLAG(XT_HMARK_CT)) { |
| if (hmark_ct_set_htuple(skb, &t, info) < 0) |
| return XT_CONTINUE; |
| } else { |
| if (hmark_pkt_set_htuple_ipv4(skb, &t, info) < 0) |
| return XT_CONTINUE; |
| } |
| |
| skb->mark = hmark_hash(&t, info); |
| return XT_CONTINUE; |
| } |
| |
| static int hmark_tg_check(const struct xt_tgchk_param *par) |
| { |
| const struct xt_hmark_info *info = par->targinfo; |
| const char *errmsg = "proto mask must be zero with L3 mode"; |
| |
| if (!info->hmodulus) |
| return -EINVAL; |
| |
| if (info->proto_mask && |
| (info->flags & XT_HMARK_FLAG(XT_HMARK_METHOD_L3))) |
| goto err; |
| |
| if (info->flags & XT_HMARK_FLAG(XT_HMARK_SPI_MASK) && |
| (info->flags & (XT_HMARK_FLAG(XT_HMARK_SPORT_MASK) | |
| XT_HMARK_FLAG(XT_HMARK_DPORT_MASK)))) |
| return -EINVAL; |
| |
| if (info->flags & XT_HMARK_FLAG(XT_HMARK_SPI) && |
| (info->flags & (XT_HMARK_FLAG(XT_HMARK_SPORT) | |
| XT_HMARK_FLAG(XT_HMARK_DPORT)))) { |
| errmsg = "spi-set and port-set can't be combined"; |
| goto err; |
| } |
| return 0; |
| err: |
| pr_info_ratelimited("%s\n", errmsg); |
| return -EINVAL; |
| } |
| |
| static struct xt_target hmark_tg_reg[] __read_mostly = { |
| { |
| .name = "HMARK", |
| .family = NFPROTO_IPV4, |
| .target = hmark_tg_v4, |
| .targetsize = sizeof(struct xt_hmark_info), |
| .checkentry = hmark_tg_check, |
| .me = THIS_MODULE, |
| }, |
| #if IS_ENABLED(CONFIG_IP6_NF_IPTABLES) |
| { |
| .name = "HMARK", |
| .family = NFPROTO_IPV6, |
| .target = hmark_tg_v6, |
| .targetsize = sizeof(struct xt_hmark_info), |
| .checkentry = hmark_tg_check, |
| .me = THIS_MODULE, |
| }, |
| #endif |
| }; |
| |
| static int __init hmark_tg_init(void) |
| { |
| return xt_register_targets(hmark_tg_reg, ARRAY_SIZE(hmark_tg_reg)); |
| } |
| |
| static void __exit hmark_tg_exit(void) |
| { |
| xt_unregister_targets(hmark_tg_reg, ARRAY_SIZE(hmark_tg_reg)); |
| } |
| |
| module_init(hmark_tg_init); |
| module_exit(hmark_tg_exit); |