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: stable@vger.kernel.org Reported-by: Yizhou Zhao Reported-by: Yuxiang Yang Reported-by: Ao Wang Reported-by: Xuewei Feng Reported-by: Qi Li Reported-by: Ke Xu Assisted-by: Claude-Code:GLM-5.2 Signed-off-by: Yizhou Zhao --- 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