[PATCH nf-next v1 2/2] netfilter: reject: don't reply to decrypted skbs

From: Todd Freed

Date: Tue Nov 10 2026 - 19:32:44 EST


on receive, skb->decrypted means that the device decrypted the skb (tls
device offload, psp). for a decrypted skb, any part of the packet which
had been encrypted should not be sent back onto the network in
cleartext.

"ipv4: icmp: don't quote decrypted payload in icmp errors" and
"ipv6: icmpv6: don't quote decrypted payload in icmpv6 errors" (net)
make the ip stack send no icmp error about a received decrypted skb.
tls and psp skbs can't be told apart reliably, since
skb_scrub_packet() drops SKB_EXT_PSP and keeps skb->decrypted, so the
rule covers both.

the reject code builds its own replies, without icmp_send() and
icmpv6_send():

- nf_reject_skb_v4_unreach() and nf_reject_skb_v6_unreach(), used by
the reject expression in netdev and bridge chains, quote up to 536
and 1220 bytes of the packet.
- the tcp resets of nf_reject_skb_v4_tcp_reset(),
nf_reject_skb_v6_tcp_reset(), nf_send_reset() and nf_send_reset6()
carry the ports and sequence numbers of the packet's tcp header,
which are encrypted with psp.

apply the same rule here:

- nf_reject_skb_v4_tcp_reset(), nf_reject_skb_v4_unreach() and their
ipv6 versions only get received skbs: the bridge and netdev reject
expressions are limited to the prerouting, input and ingress hooks.
don't send a reply for a decrypted skb.
- nf_send_reset() and nf_send_reset6() also get locally originated
skbs, from a reject rule in the output chain. don't send a reset
about a received decrypted skb. for a locally originated, decrypted
skb, only send the reset if it is routed to a local address
(RTCF_LOCAL, RTF_LOCAL), as icmp_send() and icmpv6_send() do.

a reject rule then drops a received decrypted packet without a reply, as
it does a packet it can't reply to, and conntrack isn't moved to closing
either, as with a drop rule. nf_send_unreach() and nf_send_unreach6()
use icmp_send() and icmpv6_send(), and get the rule from the patches in
net.

Signed-off-by: Todd Freed <toddfreed@xxxxxxxx>
---
net/ipv4/netfilter/nf_reject_ipv4.c | 26 ++++++++++++++++++++++++++
net/ipv6/netfilter/nf_reject_ipv6.c | 28 ++++++++++++++++++++++++++++
2 files changed, 54 insertions(+)

diff --git a/net/ipv4/netfilter/nf_reject_ipv4.c b/net/ipv4/netfilter/nf_reject_ipv4.c
index 7cb33af671e2..1ad5ada45536 100644
--- a/net/ipv4/netfilter/nf_reject_ipv4.c
+++ b/net/ipv4/netfilter/nf_reject_ipv4.c
@@ -59,6 +59,12 @@ struct sk_buff *nf_reject_skb_v4_tcp_reset(struct net *net,
if (!nf_reject_iphdr_validate(oldskb))
return NULL;

+ /* Only received skbs get here (bridge, netdev ingress): see
+ * nf_send_reset()
+ */
+ if (unlikely(skb_is_decrypted(oldskb)))
+ return NULL;
+
oth = nf_reject_ip_tcphdr_get(oldskb, &_oth, hook);
if (!oth)
return NULL;
@@ -118,6 +124,12 @@ struct sk_buff *nf_reject_skb_v4_unreach(struct net *net,
if (!nf_reject_iphdr_validate(oldskb))
return NULL;

+ /* Only received skbs get here (bridge, netdev ingress): see
+ * nf_send_reset()
+ */
+ if (unlikely(skb_is_decrypted(oldskb)))
+ return NULL;
+
/* IP header checks: fragment. */
if (ip_hdr(oldskb)->frag_off & htons(IP_OFFSET))
return NULL;
@@ -287,6 +299,16 @@ void nf_send_reset(struct net *net, struct sock *sk, struct sk_buff *oldskb,
if (skb_rtable(oldskb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))
return;

+ /* Don't send the decrypted part of an skb onto the network. A
+ * received skb was decrypted by the device with crypto offload, which
+ * in some cases encrypts the transport header. The different offloads
+ * can't be reliably distinguished, so don't reply at all. A locally
+ * originated skb to be encrypted gets a reply if the reply is routed to
+ * a local address.
+ */
+ if (unlikely(skb_is_decrypted(oldskb) && oldskb->skb_iif))
+ return;
+
nskb = alloc_skb(sizeof(struct iphdr) + sizeof(struct tcphdr) +
LL_MAX_HEADER, GFP_ATOMIC);
if (!nskb)
@@ -304,6 +326,10 @@ void nf_send_reset(struct net *net, struct sock *sk, struct sk_buff *oldskb,
if (ip_route_me_harder(net, sk, nskb, RTN_UNSPEC))
goto free_nskb;

+ if (unlikely(skb_is_decrypted(oldskb)) &&
+ !(skb_rtable(nskb)->rt_flags & RTCF_LOCAL))
+ goto free_nskb;
+
/* "Never happens" */
if (nskb->len > dst4_mtu(skb_dst(nskb)))
goto free_nskb;
diff --git a/net/ipv6/netfilter/nf_reject_ipv6.c b/net/ipv6/netfilter/nf_reject_ipv6.c
index 5e08d8529cc8..de815538ebf1 100644
--- a/net/ipv6/netfilter/nf_reject_ipv6.c
+++ b/net/ipv6/netfilter/nf_reject_ipv6.c
@@ -84,6 +84,12 @@ struct sk_buff *nf_reject_skb_v6_tcp_reset(struct net *net,
if (!nf_reject_ip6hdr_validate(oldskb))
return NULL;

+ /* Only received skbs get here (bridge, netdev ingress): see
+ * nf_send_reset6()
+ */
+ if (unlikely(skb_is_decrypted(oldskb)))
+ return NULL;
+
oth = nf_reject_ip6_tcphdr_get(oldskb, &_oth, &otcplen, hook);
if (!oth)
return NULL;
@@ -144,6 +150,12 @@ struct sk_buff *nf_reject_skb_v6_unreach(struct net *net,
if (!nf_reject_ip6hdr_validate(oldskb))
return NULL;

+ /* Only received skbs get here (bridge, netdev ingress): see
+ * nf_send_reset6()
+ */
+ if (unlikely(skb_is_decrypted(oldskb)))
+ return NULL;
+
/* Don't reply to ICMPV6_DEST_UNREACH with ICMPV6_DEST_UNREACH */
if (nf_skb_is_icmp6_unreach(oldskb))
return NULL;
@@ -331,6 +343,16 @@ void nf_send_reset6(struct net *net, struct sock *sk, struct sk_buff *oldskb,
if (!otcph)
return;

+ /* Don't send the decrypted part of an skb onto the network. A
+ * received skb was decrypted by the device with crypto offload, which
+ * in some cases encrypts the transport header. The different offloads
+ * can't be reliably distinguished, so don't reply at all. A locally
+ * originated skb to be encrypted gets a reply if the reply is routed to
+ * a local address.
+ */
+ if (unlikely(skb_is_decrypted(oldskb) && oldskb->skb_iif))
+ return;
+
memset(&fl6, 0, sizeof(fl6));
fl6.flowi6_proto = IPPROTO_TCP;
fl6.saddr = oip6h->daddr;
@@ -359,6 +381,12 @@ void nf_send_reset6(struct net *net, struct sock *sk, struct sk_buff *oldskb,
if (IS_ERR(dst))
return;

+ if (unlikely(skb_is_decrypted(oldskb)) &&
+ !(dst_rt6_info(dst)->rt6i_flags & RTF_LOCAL)) {
+ dst_release(dst);
+ return;
+ }
+
nskb = alloc_skb(LL_MAX_HEADER + sizeof(struct ipv6hdr) +
sizeof(struct tcphdr) + dst->trailer_len,
GFP_ATOMIC);
--
2.53.0-Meta