tcp_state_transition() reads the TCP header again in order to drive the
IPVS TCP state table.  For IPv6 it computes the offset with
sizeof(struct ipv6hdr), while the surrounding IPVS code uses iph->len from
ip_vs_fill_iph_skb(), where ipv6_find_hdr() has already skipped extension
headers and found the real transport header.

This makes the state machine read from the wrong offset for IPv6 TCP
packets that carry extension headers.  For example, a SYN packet with an
8-byte destination options header can be scheduled correctly by
tcp_conn_schedule(), but tcp_state_transition() reads a byte from the real
TCP sequence number as the TCP flags.  An attacker can therefore make an
uncompleted SYN move from NONE to ESTABLISHED or CLOSE, changing the
connection timeout and active/inactive destination counters.  In the
ESTABLISHED case, the half-open connection gets the 15 minute established
timeout instead of the shorter SYN_RECV timeout, which can be used for a
remote denial of service against an IPv6 IPVS TCP service.

Use ip_vs_fill_iph_skb() in tcp_state_transition() and read the TCP header
from iph.len, matching tcp_conn_schedule() and the TCP NAT handlers.  For
IPv4 and IPv6 packets without extension headers this preserves the existing
offset.

Fixes: 0bbdd42b7efa ("IPVS: Extend protocol DNAT/SNAT and state handlers")
Cc: [email protected]
Reported-by: Yizhou Zhao <[email protected]>
Reported-by: Yuxiang Yang <[email protected]>
Reported-by: Ao Wang <[email protected]>
Reported-by: Xuewei Feng <[email protected]>
Reported-by: Qi Li <[email protected]>
Reported-by: Ke Xu <[email protected]>
Assisted-by: Claude-Code:GLM-5.2
Signed-off-by: Yizhou Zhao <[email protected]>
---
 net/netfilter/ipvs/ip_vs_proto_tcp.c | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/net/netfilter/ipvs/ip_vs_proto_tcp.c 
b/net/netfilter/ipvs/ip_vs_proto_tcp.c
index 8cc0a8ce6241..6fde2165a8d6 100644
--- a/net/netfilter/ipvs/ip_vs_proto_tcp.c
+++ b/net/netfilter/ipvs/ip_vs_proto_tcp.c
@@ -581,15 +581,13 @@ tcp_state_transition(struct ip_vs_conn *cp, int direction,
                     const struct sk_buff *skb,
                     struct ip_vs_proto_data *pd)
 {
        struct tcphdr _tcph, *th;
+       struct ip_vs_iphdr iph;
 
-#ifdef CONFIG_IP_VS_IPV6
-       int ihl = cp->af == AF_INET ? ip_hdrlen(skb) : sizeof(struct ipv6hdr);
-#else
-       int ihl = ip_hdrlen(skb);
-#endif
+       if (!ip_vs_fill_iph_skb(cp->af, skb, false, &iph))
+               return;
 
-       th = skb_header_pointer(skb, ihl, sizeof(_tcph), &_tcph);
+       th = skb_header_pointer(skb, iph.len, sizeof(_tcph), &_tcph);
        if (th == NULL)
                return;
 
-- 
2.47.3


Reply via email to