[RFC net-next v2 08/18] net: ipxlat: add IPv4 packet validation
flat view
WARM1d
From: Ralf Lici <ralf@mandelbit.com>
Date: 2026-10-24 09:26:19
Also in:
lkml
Subsystem:
networking [general], the rest · Maintainers:
"David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Linus Torvalds
Validate IPv4 headers, options, transport headers, checksums and bounded ICMP quotes while caching the offsets needed by later translation stages. Signed-off-by: Ralf Lici <ralf@mandelbit.com> --- Changes since RFC v1 https://lore.kernel.org/netdev/20260319151230.655687-5-ralf@mandelbit.com/ (local) - Split IPv4 parsing out of the v1 combined packet.c implementation. - Separate unsupported packet shapes from malformed packets so an attachment can fall back before translation starts. - Bound quoted-packet parsing to the quoted datagram. - Pull the complete declared outer TCP header before later stages use it. - Treat a CHECKSUM_PARTIAL UDP packet as checksum-present even when the checksum field still contains zero. (Sashiko) - Validate ICMP checksums represented by CHECKSUM_COMPLETE rather than trusting all hardware checksum metadata. (Sashiko) net/ipxlat/Makefile | 1 + net/ipxlat/packet4.c | 410 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 net/ipxlat/packet4.c
diff --git a/net/ipxlat/Makefile b/net/ipxlat/Makefile
index 013c338a2576..f517eaa75423 100644
--- a/net/ipxlat/Makefile
+++ b/net/ipxlat/Makefile@@ -7,3 +7,4 @@ obj-$(CONFIG_NET_IPXLAT) += ipxlat.o ipxlat-objs += config.o ipxlat-objs += core.o ipxlat-objs += address.o +ipxlat-objs += packet4.o
diff --git a/net/ipxlat/packet4.c b/net/ipxlat/packet4.c
new file mode 100644
index 000000000000..590f95fb6b2c
--- /dev/null
+++ b/net/ipxlat/packet4.c@@ -0,0 +1,410 @@ +// SPDX-License-Identifier: GPL-2.0 +/* IPXLAT - Stateless IP/ICMP Translation (SIIT) engine + * + * Copyright (C) 2024- Alberto Leiva Popper <ydahhrk@gmail.com> + * Copyright (C) 2026- Mandelbit SRL + * Copyright (C) 2026- Daniel Gröber <dxld@darkboxed.org> + * + * Author: Alberto Leiva Popper <ydahhrk@gmail.com> + * Antonio Quartulli <antonio@mandelbit.com> + * Daniel Gröber <dxld@darkboxed.org> + * Ralf Lici <ralf@mandelbit.com> + */ + +#include <linux/icmp.h> + +#include <net/ipxlat.h> + +#include "packet.h" + +static bool ipxlat_v4_validate_addr(__be32 addr4) +{ + return !(ipv4_is_zeronet(addr4) || ipv4_is_loopback(addr4) || + ipv4_is_multicast(addr4) || ipv4_is_lbcast(addr4)); +} + +/* RFC 7915 Section 4.1 requires ignoring IPv4 options unless an unexpired + * LSRR/SSRR is present, in which case we must send ICMPv4 SR_FAILED. + * We intentionally treat malformed option encoding as invalid input and + * drop early instead of continuing translation. + */ +static int ipxlat_v4_srr_check(struct sk_buff *skb, const struct iphdr *hdr) +{ + const u8 *opt, *end; + u8 type, len, ptr; + + if (likely(hdr->ihl <= 5)) + return 0; + + opt = (const u8 *)(hdr + 1); + end = (const u8 *)hdr + (hdr->ihl << 2); + + while (opt < end) { + type = opt[0]; + if (type == IPOPT_END) + return 0; + if (type == IPOPT_NOOP) { + opt++; + continue; + } + + if (unlikely(end - opt < 2)) + return -EINVAL; + + len = opt[1]; + if (unlikely(len < 2 || opt + len > end)) + return -EINVAL; + + if (type == IPOPT_LSRR || type == IPOPT_SSRR) { + if (unlikely(len < 3)) + return -EINVAL; + + /* points to the beginning of the next IP addr */ + ptr = opt[2]; + if (unlikely(ptr < 4)) + return -EINVAL; + if (unlikely(ptr > len)) + return 0; + if (unlikely(ptr > len - 3)) + return -EINVAL; + + ipxlat_mark_icmp_drop(skb, ICMP_DEST_UNREACH, + ICMP_SR_FAILED, 0); + return -EINVAL; + } + + opt += len; + } + + return 0; +} + +static int ipxlat_v4_pull_l3(struct sk_buff *skb, unsigned int l3_offset, + unsigned int packet_end) +{ + const struct iphdr *iph; + unsigned int tot_len; + int l3_len; + + if (unlikely(l3_offset > packet_end || + sizeof(*iph) > packet_end - l3_offset || + !pskb_may_pull(skb, l3_offset + sizeof(*iph)))) + return -EINVAL; + + iph = (const struct iphdr *)(skb->data + l3_offset); + if (unlikely(iph->version != 4 || iph->ihl < 5)) + return -EINVAL; + + l3_len = iph->ihl << 2; + if (unlikely(l3_len > packet_end - l3_offset)) + return -EINVAL; + + /* an ICMP skb has no GSO representation, so iph_totlen also works + * for quoted inner headers without using unrelated GSO metadata + */ + tot_len = iph_totlen(skb, iph); + if (unlikely(tot_len < l3_len)) + return -EINVAL; + + if (unlikely(!pskb_may_pull(skb, l3_offset + l3_len))) + return -EINVAL; + + return l3_len; +} + +static int ipxlat_v4_pull_l4(struct sk_buff *skb, unsigned int l4_offset, + u8 l4_proto, bool *is_icmp_err) +{ + unsigned int tcp_hdr_len; + struct icmphdr *icmp; + struct udphdr *udp; + struct tcphdr *tcp; + + *is_icmp_err = false; + + switch (l4_proto) { + case IPPROTO_TCP: + if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*tcp)))) + return -EINVAL; + + tcp = (struct tcphdr *)(skb->data + l4_offset); + if (unlikely(tcp->doff < 5)) + return -EINVAL; + + tcp_hdr_len = __tcp_hdrlen(tcp); + if (unlikely(!pskb_may_pull(skb, l4_offset + tcp_hdr_len))) + return -EINVAL; + + return tcp_hdr_len; + case IPPROTO_UDP: + if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*udp)))) + return -EINVAL; + + udp = (struct udphdr *)(skb->data + l4_offset); + if (unlikely(ntohs(udp->len) < sizeof(*udp))) + return -EINVAL; + + return sizeof(struct udphdr); + case IPPROTO_ICMP: + if (unlikely(!pskb_may_pull(skb, l4_offset + sizeof(*icmp)))) + return -EINVAL; + + icmp = (struct icmphdr *)(skb->data + l4_offset); + *is_icmp_err = icmp_is_err(icmp->type); + return sizeof(struct icmphdr); + default: + return 0; + } +} + +/* ICMP errors are only required to quote the first eight transport bytes. + * That is enough for UDP and ICMP, but not for a TCP data offset/checksum. + * We leave a quoted TCP header unchanged when the quote does not contain the + * complete header. + */ +static int ipxlat_v4_pull_icmp_l4(struct sk_buff *skb, unsigned int l4_offset, + unsigned int quote_end, u8 l4_proto) +{ + struct icmphdr *icmp; + unsigned int need; + + switch (l4_proto) { + case IPPROTO_ICMP: + need = sizeof(*icmp); + break; + case IPPROTO_UDP: + need = sizeof(struct udphdr); + break; + default: + return 0; + } + + if (unlikely(l4_offset > quote_end || need > quote_end - l4_offset || + !pskb_may_pull(skb, l4_offset + need))) + return -EINVAL; + + if (l4_proto == IPPROTO_ICMP) { + icmp = (struct icmphdr *)(skb->data + l4_offset); + if (unlikely(icmp_is_err(icmp->type))) + return -EINVAL; + } + + return 0; +} + +static int ipxlat_v4_icmp_quote_end(const struct sk_buff *skb, + unsigned int payload_off, + unsigned int *quote_end) +{ + const struct icmphdr *icmp = + (const struct icmphdr *)(skb->data + payload_off - + sizeof(*icmp)); + unsigned int quote_len = icmp->un.reserved[1] << 2; + + if (!quote_len) { + *quote_end = skb->len; + return 0; + } + + /* a nonzero RFC 4884 delimiter describes at least 128 quoted bytes */ + if (unlikely(quote_len < ICMP_EXT_ORIG_DGRAM_MIN_LEN || + payload_off > skb->len || + quote_len > skb->len - payload_off)) + return -EINVAL; + + *quote_end = payload_off + quote_len; + return 0; +} + +static int ipxlat_v4_pull_icmp_inner(struct sk_buff *skb, + unsigned int inner_l3_off, + unsigned int quote_end) +{ + struct ipxlat_cb *cb = ipxlat_skb_cb(skb); + unsigned int datagram_end, inner_l4_off; + const struct iphdr *inner_l3_hdr; + int inner_l3_len, err; + + inner_l3_len = ipxlat_v4_pull_l3(skb, inner_l3_off, quote_end); + if (unlikely(inner_l3_len < 0)) + return inner_l3_len; + + inner_l3_hdr = (const struct iphdr *)(skb->data + inner_l3_off); + datagram_end = inner_l3_off + ntohs(inner_l3_hdr->tot_len); + + /* accept non-first quoted fragments: only inner L3 is translatable */ + inner_l4_off = inner_l3_off + inner_l3_len; + if (unlikely(inner_l3_off > U16_MAX || inner_l4_off > U16_MAX)) + return -E2BIG; + cb->inner_l3_offset = inner_l3_off; + cb->inner_l4_offset = inner_l4_off; + + if (unlikely(!ipxlat_is_first_frag4(inner_l3_hdr))) + return 0; + + err = ipxlat_v4_pull_icmp_l4(skb, inner_l4_off, + min(quote_end, datagram_end), + inner_l3_hdr->protocol); + if (unlikely(err < 0)) + return err; + + return 0; +} + +static int ipxlat_v4_pull_hdrs(struct sk_buff *skb) +{ + const unsigned int l3_off = skb_network_offset(skb); + struct ipxlat_cb *cb = ipxlat_skb_cb(skb); + unsigned int payload_off, quote_end; + const struct iphdr *l3_hdr; + int err, l3_len, l4_len; + + /* parse IPv4 header and get its full length including options */ + l3_len = ipxlat_v4_pull_l3(skb, l3_off, skb->len); + if (unlikely(l3_len < 0)) + return l3_len; + l3_hdr = ip_hdr(skb); + + if (unlikely(!ipxlat_v4_validate_addr(l3_hdr->daddr))) + return -EINVAL; + + /* RFC 7915 Section 4.1 */ + if (unlikely(ipxlat_v4_srr_check(skb, l3_hdr))) + return -EINVAL; + /* fragmented ICMPs cannot be translated by a stateless translator */ + if (unlikely(l3_hdr->protocol == IPPROTO_ICMP && + ip_is_fragment(l3_hdr))) + return -EINVAL; + + if (unlikely(l3_off + l3_len > U16_MAX)) + return -E2BIG; + + cb->l3_hdr_len = l3_len; + cb->l4_proto = l3_hdr->protocol; + cb->l4_off = l3_off + l3_len; + cb->payload_off = cb->l4_off; + cb->is_icmp_err = false; + + /* only non fragmented packets or first fragments have transport hdrs */ + if (unlikely(!ipxlat_is_first_frag4(l3_hdr))) { + if (unlikely(!ipxlat_v4_validate_addr(l3_hdr->saddr))) + return -EINVAL; + return 0; + } + + /* parse transport header and get its full length */ + l4_len = ipxlat_v4_pull_l4(skb, cb->l4_off, l3_hdr->protocol, + &cb->is_icmp_err); + if (unlikely(l4_len < 0)) + return l4_len; + l3_hdr = ip_hdr(skb); + + /* illegal IPv4 sources are accepted only for ICMPv4 errors */ + if (unlikely(!ipxlat_v4_validate_addr(l3_hdr->saddr) && + !cb->is_icmp_err)) + return -EINVAL; + + payload_off = cb->l4_off + l4_len; + if (unlikely(payload_off > U16_MAX)) + return -E2BIG; + cb->payload_off = payload_off; + + /* validate the quoted packet if it's an ICMP error */ + if (unlikely(cb->is_icmp_err)) { + DEBUG_NET_WARN_ON_ONCE(cb->l4_proto != IPPROTO_ICMP); + + err = ipxlat_v4_icmp_quote_end(skb, cb->payload_off, + "e_end); + if (unlikely(err)) + return err; + + err = ipxlat_v4_pull_icmp_inner(skb, cb->payload_off, + quote_end); + if (unlikely(err)) + return err; + } + + return 0; +} + +static int ipxlat_v4_validate_icmp_csum(const struct sk_buff *skb, + unsigned int l4_off) +{ + __sum16 csum; + + switch (skb->ip_summed) { + case CHECKSUM_UNNECESSARY: + case CHECKSUM_PARTIAL: + return 0; + case CHECKSUM_COMPLETE: + return csum_fold(skb->csum) ? -EINVAL : 0; + case CHECKSUM_NONE: + break; + default: + return -EINVAL; + } + + /* compute checksum over ICMP header and payload, then fold to 16-bit + * Internet checksum to validate it + */ + csum = csum_fold(skb_checksum(skb, l4_off, skb->len - l4_off, 0)); + return unlikely(csum) ? -EINVAL : 0; +} + +int ipxlat_v4_validate_skb(struct sk_buff *skb) +{ + struct ipxlat_cb *cb = ipxlat_skb_cb(skb); + unsigned int udp_len; + struct iphdr *l3_hdr; + struct udphdr *udph; + int err; + + err = ipxlat_v4_pull_hdrs(skb); + if (unlikely(err)) + return err; + + if (unlikely(cb->is_icmp_err)) { + /* Translation path recomputes ICMPv6 checksum from scratch. + * Validate here so a corrupted ICMPv4 error is not converted + * into a translated packet with a valid checksum. + */ + return ipxlat_v4_validate_icmp_csum(skb, cb->l4_off); + } + + l3_hdr = ip_hdr(skb); + if (unlikely(!ipxlat_is_first_frag4(l3_hdr))) + return 0; + + if (cb->l4_proto != IPPROTO_UDP) + return 0; + + udph = (struct udphdr *)(skb->data + cb->l4_off); + /* CHECKSUM_PARTIAL is authoritative even when the checksum field is + * zero, translation will simply reseed it for the IPv6 pseudo-header + */ + if (skb->ip_summed == CHECKSUM_PARTIAL) + return 0; + + if (likely(udph->check != 0)) + return 0; + + /* We are in the path where the UDP header is present (unfragmented + * packets or first fragments) and its checksum is zero. Fragmented + * checksum-less IPv4 UDP is rejected because 4->6 cannot reliably + * translate it. + * We record an administrative-prohibited error so an ingress harness + * can tell the sender why it was dropped. + */ + if (unlikely(ip_is_fragment(l3_hdr))) { + ipxlat_mark_icmp_drop(skb, ICMP_DEST_UNREACH, ICMP_PKT_FILTERED, + 0); + return -EINVAL; + } + + /* udph->len bounds the span used to compute replacement checksum */ + udp_len = ntohs(udph->len); + if (unlikely(udp_len > skb->len - cb->l4_off)) + return -EINVAL; + + return 0; +}
--
2.55.0