Netdev List
 help / color / mirror / Atom feed
* [PATCH 0/4] ipvs: add per-service secure_tcp
@ 2026-09-12  1:32 Adriano Cordova
  2026-09-12  1:32 ` [PATCH 1/4] ipvs: add flags for per-service secure TCP state table Adriano Cordova
                   ` (3 more replies)
  0 siblings, 4 replies; 5+ messages in thread
From: Adriano Cordova @ 2026-09-12  1:32 UTC (permalink / raw)
  To: Simon Horman, Julian Anastasov
  Cc: Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Shuah Khan, netdev, lvs-devel, netfilter-devel, Adriano Cordova

IPVS currently exposes secure_tcp as a per-netns sysctl. It switches the
TCP state machine to the hardened tcp_states_dos table.

These patches make it per-service: a virtual service can set
IP_VS_SVC_F_SECURE_TCP which is passed into IP_VS_CONN_F_SECURE_TCP at
connection creation. set_tcp_state() then selects tcp_states_dos for those
connections with IP_VS_CONN_F_SECURE_TCP set and keeps pd->tcp_state_table
(the netns default, including the nomem floor) otherwise.

The flag is part of BACKUP_MASK, so it is preserved on sync to backups.

  1. uapi: define the per-service secure_tcp flags
  2. carry the flag on every connection-creation path (scheduler,
     persistence, RS-initiated, cache-bypass) and on FTP data channels
  3. honor it in the TCP state machine, resolving the stale FIXME
  4. kselftest contrasting a secure vs. a plain service.

Not sure about patch 4... could be dropped or absorbed into ipvs.sh

Adriano Cordova (4):
  ipvs: add flags for per-service secure TCP state table
  ipvs: stamp per-service secure_tcp on new connections
  ipvs: tcp: enable per-connection secure_tcp in state machine
  selftests: netfilter: ipvs: add per-service secure_tcp test

 include/uapi/linux/ip_vs.h                    |   5 +-
 net/netfilter/ipvs/ip_vs_core.c               |  23 +-
 net/netfilter/ipvs/ip_vs_ftp.c                |   8 +-
 net/netfilter/ipvs/ip_vs_proto_tcp.c          |  16 +-
 .../testing/selftests/net/netfilter/Makefile  |   6 +
 .../selftests/net/netfilter/gen_tcp_probe.c   | 127 +++++++
 .../net/netfilter/ipvs_secure_tcp.sh          | 158 +++++++++
 .../net/netfilter/ipvs_secure_tcp_mln.c       | 310 ++++++++++++++++++
 8 files changed, 638 insertions(+), 15 deletions(-)
 create mode 100644 tools/testing/selftests/net/netfilter/gen_tcp_probe.c
 create mode 100755 tools/testing/selftests/net/netfilter/ipvs_secure_tcp.sh
 create mode 100644 tools/testing/selftests/net/netfilter/ipvs_secure_tcp_mln.c

-- 
2.51.0


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 1/4] ipvs: add flags for per-service secure TCP state table
  2026-09-12  1:32 [PATCH 0/4] ipvs: add per-service secure_tcp Adriano Cordova
@ 2026-09-12  1:32 ` Adriano Cordova
  2026-09-12  1:32 ` [PATCH 2/4] ipvs: stamp per-service secure_tcp on new connections Adriano Cordova
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 5+ messages in thread
From: Adriano Cordova @ 2026-09-12  1:32 UTC (permalink / raw)
  To: Simon Horman, Julian Anastasov
  Cc: Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Shuah Khan, netdev, lvs-devel, netfilter-devel, Adriano Cordova

Add the flag IP_VS_SVC_F_SECURE_TCP to mark a virtual service
for the DoS hardened TCP connection state table, and the
flag IP_VS_CONN_F_SECURE_TCP to mark a connection and then
pass to the TCP state machine.

The connection flag is included in IP_VS_CONN_F_BACKUP_MASK,
so it is preserved on the backup node.

Signed-off-by: Adriano Cordova <adrianox@gmail.com>
---
 include/uapi/linux/ip_vs.h | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/include/uapi/linux/ip_vs.h b/include/uapi/linux/ip_vs.h
index 2c37c6ac7525..34fcfaf13cd3 100644
--- a/include/uapi/linux/ip_vs.h
+++ b/include/uapi/linux/ip_vs.h
@@ -27,6 +27,7 @@
 
 #define IP_VS_SVC_F_SCHED_SH_FALLBACK	IP_VS_SVC_F_SCHED1 /* SH fallback */
 #define IP_VS_SVC_F_SCHED_SH_PORT	IP_VS_SVC_F_SCHED2 /* SH use port */
+#define IP_VS_SVC_F_SECURE_TCP	0x0040		/* use the hardened TCP table */
 
 /*
  *      IPVS sync daemon states
@@ -89,6 +90,7 @@
 #define IP_VS_CONN_F_NO_CPORT	0x0800		/* no client port set yet */
 #define IP_VS_CONN_F_TEMPLATE	0x1000		/* template, not connection */
 #define IP_VS_CONN_F_ONE_PACKET	0x2000		/* forward only one packet */
+#define IP_VS_CONN_F_SECURE_TCP	0x0008		/* use the hardened TCP table */
 
 /* Initial bits allowed in backup server */
 #define IP_VS_CONN_F_BACKUP_MASK (IP_VS_CONN_F_FWD_MASK | \
@@ -96,7 +98,8 @@
 				  IP_VS_CONN_F_INACTIVE | \
 				  IP_VS_CONN_F_SEQ_MASK | \
 				  IP_VS_CONN_F_NO_CPORT | \
-				  IP_VS_CONN_F_TEMPLATE \
+				  IP_VS_CONN_F_TEMPLATE | \
+				  IP_VS_CONN_F_SECURE_TCP \
 				 )
 
 /* Bits allowed to update in backup server */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH 2/4] ipvs: stamp per-service secure_tcp on new connections
  2026-09-12  1:32 [PATCH 0/4] ipvs: add per-service secure_tcp Adriano Cordova
  2026-09-12  1:32 ` [PATCH 1/4] ipvs: add flags for per-service secure TCP state table Adriano Cordova
@ 2026-09-12  1:32 ` Adriano Cordova
  2026-09-12  1:32 ` [PATCH 3/4] ipvs: tcp: enable per-connection secure_tcp in state machine Adriano Cordova
  2026-09-12  1:32 ` [PATCH 4/4] selftests: netfilter: ipvs: add per-service secure_tcp test Adriano Cordova
  3 siblings, 0 replies; 5+ messages in thread
From: Adriano Cordova @ 2026-09-12  1:32 UTC (permalink / raw)
  To: Simon Horman, Julian Anastasov
  Cc: Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Shuah Khan, netdev, lvs-devel, netfilter-devel, Adriano Cordova

Set the IP_VS_SVC_F_SECURE_TCP capability into
IP_VS_CONN_F_SECURE_TCP when a connection (or a persistent
template) is created for a service.

FTP data channels are created by the ftp app separetely,
outside the paths above, so propagate the flag from the
control connection.

Signed-off-by: Adriano Cordova <adrianox@gmail.com>
---
 net/netfilter/ipvs/ip_vs_core.c | 23 +++++++++++++++++++----
 net/netfilter/ipvs/ip_vs_ftp.c  |  8 +++++---
 2 files changed, 24 insertions(+), 7 deletions(-)

diff --git a/net/netfilter/ipvs/ip_vs_core.c b/net/netfilter/ipvs/ip_vs_core.c
index ba0957798bad..eead1b992dd9 100644
--- a/net/netfilter/ipvs/ip_vs_core.c
+++ b/net/netfilter/ipvs/ip_vs_core.c
@@ -52,6 +52,13 @@
 #include <linux/indirect_call_wrapper.h>
 
 
+/* Encode the per-service secure_tcp capability into a connection flag */
+static inline unsigned int ip_vs_conn_secure_tcp_flags(struct ip_vs_service *svc)
+{
+	return (svc->flags & IP_VS_SVC_F_SECURE_TCP) ?
+		IP_VS_CONN_F_SECURE_TCP : 0;
+}
+
 EXPORT_SYMBOL(register_ip_vs_scheduler);
 EXPORT_SYMBOL(unregister_ip_vs_scheduler);
 EXPORT_SYMBOL(ip_vs_proto_name);
@@ -546,7 +553,9 @@ ip_vs_sched_persist(struct ip_vs_service *svc,
 		 * and thus param.pe_data will be destroyed
 		 * when the template expires */
 		ct = ip_vs_conn_new(&param, dest->af, &dest->addr, dport,
-				    IP_VS_CONN_F_TEMPLATE, dest, skb->mark);
+				    IP_VS_CONN_F_TEMPLATE |
+				    ip_vs_conn_secure_tcp_flags(svc), dest,
+				    skb->mark);
 		if (ct == NULL) {
 			kfree(param.pe_data);
 			*ignored = -1;
@@ -567,6 +576,7 @@ ip_vs_sched_persist(struct ip_vs_service *svc,
 	flags = (svc->flags & IP_VS_SVC_F_ONEPACKET
 		 && iph->protocol == IPPROTO_UDP) ?
 		IP_VS_CONN_F_ONE_PACKET : 0;
+	flags |= ip_vs_conn_secure_tcp_flags(svc);
 
 	/*
 	 *    Create a new connection according to the template
@@ -714,6 +724,7 @@ ip_vs_schedule(struct ip_vs_service *svc, struct sk_buff *skb,
 	flags = (svc->flags & IP_VS_SVC_F_ONEPACKET
 		 && iph->protocol == IPPROTO_UDP) ?
 		IP_VS_CONN_F_ONE_PACKET : 0;
+	flags |= ip_vs_conn_secure_tcp_flags(svc);
 
 	/*
 	 *    Create a connection entry.
@@ -779,9 +790,10 @@ int ip_vs_leave(struct ip_vs_service *svc, struct sk_buff *skb,
 	    ip_vs_addr_is_unicast(net, svc->af, &iph->daddr)) {
 		int ret;
 		struct ip_vs_conn *cp;
-		unsigned int flags = (svc->flags & IP_VS_SVC_F_ONEPACKET &&
+		unsigned int flags = ((svc->flags & IP_VS_SVC_F_ONEPACKET &&
 				      iph->protocol == IPPROTO_UDP) ?
-				      IP_VS_CONN_F_ONE_PACKET : 0;
+				      IP_VS_CONN_F_ONE_PACKET : 0) |
+				      ip_vs_conn_secure_tcp_flags(svc);
 		union nf_inet_addr daddr = { .all = { 0, 0, 0, 0 } };
 
 		/* create a new connection entry */
@@ -1350,7 +1362,9 @@ struct ip_vs_conn *ip_vs_new_conn_out(struct ip_vs_service *svc,
 		/* check if template exists and points to the same dest */
 		if (!ct || !ip_vs_check_template(ct, dest)) {
 			ct = ip_vs_conn_new(&param, dest->af, daddr, dport,
-					    IP_VS_CONN_F_TEMPLATE, dest, 0);
+					    IP_VS_CONN_F_TEMPLATE |
+					    ip_vs_conn_secure_tcp_flags(svc),
+					    dest, 0);
 			if (!ct) {
 				kfree(param.pe_data);
 				return NULL;
@@ -1364,6 +1378,7 @@ struct ip_vs_conn *ip_vs_new_conn_out(struct ip_vs_service *svc,
 	/* connection flags */
 	flags = ((svc->flags & IP_VS_SVC_F_ONEPACKET) &&
 		 iph->protocol == IPPROTO_UDP) ? IP_VS_CONN_F_ONE_PACKET : 0;
+	flags |= ip_vs_conn_secure_tcp_flags(svc);
 	/* create connection */
 	ip_vs_conn_fill_param(svc->ipvs, svc->af, iph->protocol,
 			      caddr, cport, vaddr, vport, &param);
diff --git a/net/netfilter/ipvs/ip_vs_ftp.c b/net/netfilter/ipvs/ip_vs_ftp.c
index b315c608fda4..73d2e7904303 100644
--- a/net/netfilter/ipvs/ip_vs_ftp.c
+++ b/net/netfilter/ipvs/ip_vs_ftp.c
@@ -330,7 +330,8 @@ static int ip_vs_ftp_out(struct ip_vs_app *app, struct ip_vs_conn *cp,
 				      0, &cp->vaddr, port, &p);
 		n_cp = ip_vs_conn_new(&p, cp->af, &from, port,
 				      IP_VS_CONN_F_NO_CPORT |
-				      IP_VS_CONN_F_NFCT,
+				      IP_VS_CONN_F_NFCT |
+				      (cp->flags & IP_VS_CONN_F_SECURE_TCP),
 				      cp->dest, skb->mark);
 		if (!n_cp)
 			return 0;
@@ -535,8 +536,9 @@ static int ip_vs_ftp_in(struct ip_vs_app *app, struct ip_vs_conn *cp,
 		if (!n_cp) {
 			n_cp = ip_vs_conn_new(&p, cp->af, &cp->daddr,
 					      htons(ntohs(cp->dport)-1),
-					      IP_VS_CONN_F_NFCT, cp->dest,
-					      skb->mark);
+					      IP_VS_CONN_F_NFCT |
+					      (cp->flags & IP_VS_CONN_F_SECURE_TCP),
+					      cp->dest, skb->mark);
 			if (!n_cp)
 				return 0;
 
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH 3/4] ipvs: tcp: enable per-connection secure_tcp in state machine
  2026-09-12  1:32 [PATCH 0/4] ipvs: add per-service secure_tcp Adriano Cordova
  2026-09-12  1:32 ` [PATCH 1/4] ipvs: add flags for per-service secure TCP state table Adriano Cordova
  2026-09-12  1:32 ` [PATCH 2/4] ipvs: stamp per-service secure_tcp on new connections Adriano Cordova
@ 2026-09-12  1:32 ` Adriano Cordova
  2026-09-12  1:32 ` [PATCH 4/4] selftests: netfilter: ipvs: add per-service secure_tcp test Adriano Cordova
  3 siblings, 0 replies; 5+ messages in thread
From: Adriano Cordova @ 2026-09-12  1:32 UTC (permalink / raw)
  To: Simon Horman, Julian Anastasov
  Cc: Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Shuah Khan, netdev, lvs-devel, netfilter-devel, Adriano Cordova

Let set_tcp_state select tcp_states_dos when the connection
carries IP_VS_CONN_F_SECURE_TCP, otherwise it keep using the
global pd->tcp_state_table (the netns default / or if nomem).

Signed-off-by: Adriano Cordova <adrianox@gmail.com>
---
 net/netfilter/ipvs/ip_vs_proto_tcp.c | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/net/netfilter/ipvs/ip_vs_proto_tcp.c b/net/netfilter/ipvs/ip_vs_proto_tcp.c
index fec0e8b47b71..3b9a2c8e9a52 100644
--- a/net/netfilter/ipvs/ip_vs_proto_tcp.c
+++ b/net/netfilter/ipvs/ip_vs_proto_tcp.c
@@ -451,11 +451,10 @@ static void tcp_timeout_change(struct ip_vs_proto_data *pd, int flags)
 	int on = (flags & 1);		/* secure_tcp */
 
 	/*
-	** FIXME: change secure_tcp to independent sysctl var
-	** or make it per-service or per-app because it is valid
-	** for most if not for all of the applications. Something
-	** like "capabilities" (flags) for each object.
-	*/
+	 * This remains the netns-wide default / global floor (e.g. when
+	 * memory pressure kicks in). Per-service hardening is now carried
+	 * by IP_VS_CONN_F_SECURE_TCP on each connection (set_tcp_state).
+	 */
 	pd->tcp_state_table = (on ? tcp_states_dos : tcp_states);
 }
 
@@ -479,6 +478,7 @@ set_tcp_state(struct ip_vs_proto_data *pd, struct ip_vs_conn *cp,
 	int state_idx;
 	int new_state = IP_VS_TCP_S_CLOSE;
 	int state_off = tcp_state_off[direction];
+	const struct tcp_states_t *table;
 
 	/*
 	 *    Update state offset to INPUT_ONLY if necessary
@@ -496,8 +496,10 @@ set_tcp_state(struct ip_vs_proto_data *pd, struct ip_vs_conn *cp,
 		goto tcp_state_out;
 	}
 
-	new_state =
-		pd->tcp_state_table[state_off+state_idx].next_state[cp->state];
+	table = pd->tcp_state_table;
+	if (cp->flags & IP_VS_CONN_F_SECURE_TCP)
+		table = tcp_states_dos;
+	new_state = table[state_off + state_idx].next_state[cp->state];
 
   tcp_state_out:
 	if (new_state != cp->state) {
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH 4/4] selftests: netfilter: ipvs: add per-service secure_tcp test
  2026-09-12  1:32 [PATCH 0/4] ipvs: add per-service secure_tcp Adriano Cordova
                   ` (2 preceding siblings ...)
  2026-09-12  1:32 ` [PATCH 3/4] ipvs: tcp: enable per-connection secure_tcp in state machine Adriano Cordova
@ 2026-09-12  1:32 ` Adriano Cordova
  3 siblings, 0 replies; 5+ messages in thread
From: Adriano Cordova @ 2026-09-12  1:32 UTC (permalink / raw)
  To: Simon Horman, Julian Anastasov
  Cc: Pablo Neira Ayuso, Florian Westphal, Phil Sutter,
	David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Shuah Khan, netdev, lvs-devel, netfilter-devel, Adriano Cordova

Two services share a VIP, one carrying IP_VS_SVC_F_SECURE_TCP.
A bare SYN+ACK suffices to test the state machine: the normal
service reaches ESTABLISHED, but the secure one stays in SYN_RECV.

Assisted-by: opencode
Signed-off-by: Adriano Cordova <adrianox@gmail.com>
---
 .../testing/selftests/net/netfilter/Makefile  |   6 +
 .../selftests/net/netfilter/gen_tcp_probe.c   | 127 +++++++
 .../net/netfilter/ipvs_secure_tcp.sh          | 158 +++++++++
 .../net/netfilter/ipvs_secure_tcp_mln.c       | 310 ++++++++++++++++++
 4 files changed, 601 insertions(+)
 create mode 100644 tools/testing/selftests/net/netfilter/gen_tcp_probe.c
 create mode 100755 tools/testing/selftests/net/netfilter/ipvs_secure_tcp.sh
 create mode 100644 tools/testing/selftests/net/netfilter/ipvs_secure_tcp_mln.c

diff --git a/tools/testing/selftests/net/netfilter/Makefile b/tools/testing/selftests/net/netfilter/Makefile
index f88dd4ef8d26..fad05afadd41 100644
--- a/tools/testing/selftests/net/netfilter/Makefile
+++ b/tools/testing/selftests/net/netfilter/Makefile
@@ -20,6 +20,7 @@ TEST_PROGS := \
 	conntrack_tcp_unreplied.sh \
 	conntrack_vrf.sh \
 	ipvs.sh \
+	ipvs_secure_tcp.sh \
 	nf_conntrack_packetdrill.sh \
 	nf_nat_edemux.sh \
 	nft_audit.sh \
@@ -50,6 +51,8 @@ TEST_GEN_FILES = \
 	connect_close \
 	conntrack_dump_flush \
 	conntrack_reverse_clash \
+	gen_tcp_probe \
+	ipvs_secure_tcp_mln \
 	nf_queue \
 	sctp_collision \
 	udpclash \
@@ -60,6 +63,9 @@ include ../../lib.mk
 $(OUTPUT)/nf_queue: CFLAGS += $(MNL_CFLAGS)
 $(OUTPUT)/nf_queue: LDLIBS += $(MNL_LDLIBS)
 
+$(OUTPUT)/ipvs_secure_tcp_mln: CFLAGS += $(MNL_CFLAGS)
+$(OUTPUT)/ipvs_secure_tcp_mln: LDLIBS += $(MNL_LDLIBS)
+
 $(OUTPUT)/conntrack_dump_flush: CFLAGS += $(MNL_CFLAGS)
 $(OUTPUT)/conntrack_dump_flush: LDLIBS += $(MNL_LDLIBS)
 $(OUTPUT)/udpclash: LDLIBS += -lpthread
diff --git a/tools/testing/selftests/net/netfilter/gen_tcp_probe.c b/tools/testing/selftests/net/netfilter/gen_tcp_probe.c
new file mode 100644
index 000000000000..d62bfdce70c0
--- /dev/null
+++ b/tools/testing/selftests/net/netfilter/gen_tcp_probe.c
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Send a TCP SYN then a TCP ACK (no SYN-ACK, no data) to the VIP.
+ * IPVS's TCP state machine only inspects SYN/FIN/ACK/RST bits, so this
+ * exercises the INPUT-direction state transition:
+ *
+ *   SYN:  NONE -> SYN_RECV
+ *   ACK:  SYN_RECV -> ESTABLISHED   (tcp_states, normal)
+ *         SYN_RECV -> SYN_RECV      (tcp_states_dos, secure_tcp)
+ *
+ * Requires CAP_NET_RAW.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdint.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <netinet/ip.h>
+#include <netinet/tcp.h>
+#include <linux/if_ether.h>
+
+static inline uint16_t csump(const void *data, size_t len)
+{
+	const uint16_t *p = data;
+	uint32_t sum = 0;
+
+	while (len > 1) {
+		sum += *p++;
+		len -= 2;
+	}
+	if (len)
+		sum += *(const uint8_t *)p;
+	while (sum >> 16)
+		sum = (sum & 0xffff) + (sum >> 16);
+	return ~sum;
+}
+
+static void send_seg(int fd, const struct in_addr *sip, uint16_t sport,
+		     const struct in_addr *dip, uint16_t dport,
+		     uint32_t seq, int syn, int ack)
+{
+	uint8_t pkt[sizeof(struct iphdr) + sizeof(struct tcphdr)] = { 0 };
+	struct iphdr *ip = (struct iphdr *)pkt;
+	struct tcphdr *tcp = (struct tcphdr *)(pkt + sizeof(struct iphdr));
+	struct sockaddr_in dst;
+
+	ip->version = 4;
+	ip->ihl = 5;
+	ip->tot_len = htons(sizeof(pkt));
+	ip->id = htons((uint16_t)(seq & 0xffff));
+	ip->ttl = 64;
+	ip->protocol = IPPROTO_TCP;
+	ip->saddr = sip->s_addr;
+	ip->daddr = dip->s_addr;
+
+	tcp->source = sport;
+	tcp->dest = dport;
+	tcp->seq = htonl(seq);
+	tcp->ack_seq = htonl(seq + 1);
+	tcp->doff = 5;
+	if (syn)
+		tcp->syn = 1;
+	if (ack)
+		tcp->ack = 1;
+	tcp->window = htons(1024);
+
+	ip->check = csump(ip, sizeof(struct iphdr));
+	/* pseudo header for TCP checksum */
+	{
+		uint8_t ph[12];
+
+		memcpy(ph, &ip->saddr, 4);
+		memcpy(ph + 4, &ip->daddr, 4);
+		ph[8] = 0;
+		ph[9] = IPPROTO_TCP;
+		ph[10] = (sizeof(struct tcphdr) >> 8) & 0xff;
+		ph[11] = sizeof(struct tcphdr) & 0xff;
+
+		uint8_t tcpbuf[12 + sizeof(struct tcphdr)];
+
+		memcpy(tcpbuf, ph, 12);
+		memcpy(tcpbuf + 12, tcp, sizeof(struct tcphdr));
+		tcp->check = csump(tcpbuf, sizeof(tcpbuf));
+	}
+
+	memset(&dst, 0, sizeof(dst));
+	dst.sin_family = AF_INET;
+	dst.sin_addr = *dip;
+	dst.sin_port = dport;
+	sendto(fd, pkt, sizeof(pkt), 0, (struct sockaddr *)&dst,
+	       sizeof(dst));
+}
+
+int main(int argc, char *argv[])
+{
+	struct in_addr sip, dip;
+	uint16_t sport, dport;
+	int fd, one = 1;
+	uint32_t seq = 0x12345678;
+
+	if (argc != 5) {
+		fprintf(stderr, "usage: %s <src_ip> <src_port> <dst_ip> <dst_port>\n",
+			argv[0]);
+		return 2;
+	}
+	inet_pton(AF_INET, argv[1], &sip);
+	sport = htons((uint16_t)atoi(argv[2]));
+	inet_pton(AF_INET, argv[3], &dip);
+	dport = htons((uint16_t)atoi(argv[4]));
+
+	fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
+	if (fd < 0) {
+		perror("raw socket");
+		return 1;
+	}
+	setsockopt(fd, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one));
+
+	send_seg(fd, &sip, sport, &dip, dport, seq, 1, 0);
+	usleep(100000);
+	send_seg(fd, &sip, sport, &dip, dport, seq + 1, 0, 1);
+
+	close(fd);
+	return 0;
+}
diff --git a/tools/testing/selftests/net/netfilter/ipvs_secure_tcp.sh b/tools/testing/selftests/net/netfilter/ipvs_secure_tcp.sh
new file mode 100755
index 000000000000..b079c0fe6b79
--- /dev/null
+++ b/tools/testing/selftests/net/netfilter/ipvs_secure_tcp.sh
@@ -0,0 +1,158 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Runtime test for per-service secure_tcp (IP_VS_SVC_F_SECURE_TCP).
+#
+# Sets up the same 3-namespace topology as ipvs.sh
+# but checks the TCP state machine, not data forwarding.  Two
+# identical TCP services are added on the same VIP on different ports,
+# one is marked secure_tcp, the other is not. For each a bare SYN is
+# followed by a bare ACK (no SYN-ACK / no data).  IPVS classifies the
+# connection from the flag bits:
+#   * normal service:  SYN -> SYN_RECV, ACK -> ESTABLISHED
+#   * secure_tcp service:  SYN -> SYN_RECV, ACK -> SYN_RECV
+# This test checks that this is the case via `ipvsadm -Lnc`.
+#
+# Requires root, netns, ipvsadm, nft, and the built helpers
+# ipvs_secure_tcp_mln and gen_tcp_probe.
+
+source lib.sh
+
+ret=0
+readonly vip="207.175.44.110"
+readonly gip="10.0.0.1"
+readonly dip="172.16.0.1"
+readonly rip="172.16.0.2"
+readonly cip="10.0.0.2"
+readonly sip="10.0.0.3"
+readonly port_secure=8081
+readonly port_plain=8080
+
+GREEN='\033[0;92m'
+RED='\033[0;31m'
+NC='\033[0m'
+
+checktool "ipvsadm -v" "run test without ipvsadm"
+checktool "nft --version" "run test without nft"
+
+setup() {
+	setup_ns ns0 ns1 ns2
+
+	ip link add veth01 netns "${ns0}" type veth peer name veth10 netns "${ns1}"
+	ip link add veth02 netns "${ns0}" type veth peer name veth20 netns "${ns2}"
+	ip link add veth12 netns "${ns1}" type veth peer name veth21 netns "${ns2}"
+
+	ip netns exec "${ns0}" ip link set veth01 up
+	ip netns exec "${ns0}" ip link set veth02 up
+	ip netns exec "${ns0}" ip link add br0 type bridge
+	ip netns exec "${ns0}" ip link set veth01 master br0
+	ip netns exec "${ns0}" ip link set veth02 master br0
+	ip netns exec "${ns0}" ip link set br0 up
+	ip netns exec "${ns0}" ip addr add "${cip}/24" dev br0
+
+	ip netns exec "${ns1}" ip link set veth10 up
+	ip netns exec "${ns1}" ip addr add "${gip}/24" dev veth10
+	ip netns exec "${ns1}" ip link set veth12 up
+	ip netns exec "${ns1}" ip addr add "${dip}/24" dev veth12
+	ip netns exec "${ns1}" ip link set lo up
+	ip netns exec "${ns1}" ip addr add "${vip}/32" dev lo:1
+	ip netns exec "${ns1}" sysctl -qw net.ipv4.ip_forward=1
+
+	ip netns exec "${ns2}" ip link set veth20 up
+	ip netns exec "${ns2}" ip addr add "${sip}/24" dev veth20
+	ip netns exec "${ns2}" ip link set veth21 up
+	ip netns exec "${ns2}" ip addr add "${rip}/24" dev veth21
+
+	ip netns exec "${ns2}" ip addr add "${vip}/32" dev lo:1
+
+	ip netns exec "${ns0}" ip route add "${vip}/32" via "${gip}" dev br0
+
+	# load ipvs, then the rr scheduler (separate calls: modprobe treats
+	# the second name as a module parameter, not a second module)
+	ip netns exec "${ns1}" modprobe ip_vs
+	ip netns exec "${ns1}" modprobe ip_vs_rr
+
+	sleep 1
+}
+
+cleanup() {
+	cleanup_all_ns
+}
+
+# State of the connection to the VIP:port, from `ipvsadm -Lnc`.
+# Fields: pro  expire  state  source  virtual  destination
+conn_state() {
+	local vport=$1
+	ip netns exec "${ns1}" ipvsadm -Lnc 2>/dev/null |
+		awk -v vt="${vip}:${vport}" '$5==vt { print $3; exit }'
+}
+
+assert_state() {
+	local port=$1 want=$2
+	local got
+	got="$(conn_state "$port")"
+	echo "  vip ${vip}:${port}: state=${got:-?}"
+	if [ "${got:-}" != "$want" ]; then
+		echo -e "${RED}FAIL${NC}: vip ${vip}:${port} expected state" \
+			"${want}, got ${got:-none}"
+		ret=1
+	fi
+}
+
+test_secure() {
+	local bin probe
+
+	# Register the two services (secure_tcp on the secure port)
+	bin="$(pwd)/ipvs_secure_tcp_mln"
+	probe="$(pwd)/gen_tcp_probe"
+	ip netns exec "${ns1}" "$bin" add "${vip}" "${port_secure}" secure
+	ip netns exec "${ns1}" "$bin" add "${vip}" "${port_plain}" plain
+
+	# Add a real server to both services.  Use NAT (-m): in DR the conn gets
+	# IP_VS_CONN_F_NOOUTPUT, which makes the client ACK an INPUT_ONLY event
+	# and even tcp_states_dos promotes to ESTABLISHED, hiding the difference.
+	ip netns exec "${ns1}" ipvsadm -a -m -t "${vip}:${port_secure}" -r "${rip}:${port_secure}"
+	ip netns exec "${ns1}" ipvsadm -a -m -t "${vip}:${port_plain}" -r "${rip}:${port_plain}"
+
+	# verify the flag was actually set
+	local got
+	got="$(ip netns exec "${ns1}" "$bin" get "${vip}" "${port_secure}")"
+	echo "  secured service reports: ${got}"
+	echo "${got}" | grep -q "secure_tcp=1" ||
+		{ echo -e "${RED}FAIL${NC}: flag not set"; ret=1; }
+	got="$(ip netns exec "${ns1}" "$bin" get "${vip}" "${port_plain}")"
+	echo "${got}" | grep -q "secure_tcp=0" ||
+		{ echo -e "${RED}FAIL${NC}: flag unexpectedly set"; ret=1; }
+
+	# Drop any SYN on the real server so it stays silent (no RST that
+	# would interfere with the state-machine observation).
+	ip netns exec "${ns2}" nft add table inet filter
+	ip netns exec "${ns2}" nft add chain inet filter probe \
+		'{ type filter hook input priority 0; }'
+	ip netns exec "${ns2}" nft add rule inet filter probe \
+		tcp dport '{ '"${port_secure}"', '"${port_plain}"' }' drop
+
+	# Push SYN then ACK to each service from the client
+	ip netns exec "${ns0}" "$probe" "${cip}" 40000 "${vip}" "${port_secure}"
+	ip netns exec "${ns0}" "$probe" "${cip}" 40001 "${vip}" "${port_plain}"
+	sleep 1
+
+	echo "Testing per-service secure_tcp..."
+	echo "  --- connection table (ipvsadm -Lnc) ---"
+	ip netns exec "${ns1}" ipvsadm -Lnc 2>/dev/null
+	echo "  --- end connection table ---"
+	assert_state "${port_plain}" ESTABLISHED
+	assert_state "${port_secure}" SYN_RECV
+}
+
+trap cleanup EXIT
+
+setup
+test_secure
+
+if [ "$ret" -ne 0 ]; then
+	echo -e "$(basename $0): ${RED}FAIL${NC}"
+	exit 1
+fi
+echo -e "$(basename $0): ${GREEN}PASS${NC}"
+exit 0
diff --git a/tools/testing/selftests/net/netfilter/ipvs_secure_tcp_mln.c b/tools/testing/selftests/net/netfilter/ipvs_secure_tcp_mln.c
new file mode 100644
index 000000000000..c15a3c28e6fe
--- /dev/null
+++ b/tools/testing/selftests/net/netfilter/ipvs_secure_tcp_mln.c
@@ -0,0 +1,310 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * libmnl helper to set/query the per-service secure_tcp flag
+ * (IP_VS_SVC_F_SECURE_TCP), which ipvsadm does not expose.
+ *
+ * Usage:
+ *   ipvs_secure_tcp_mln add <vip> <port> <secure|plain>
+ *       Create a TCP virtual service (scheduler "rr") with the flag either
+ *       set or not.  Add real servers afterwards with:
+ *           ipvsadm -a -t <vip>:<port> -r <rs>:<port>
+ *   ipvs_secure_tcp_mln get <vip> <port>
+ *       Print "secure_tcp=<0|1>" for the service.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+#include <linux/netlink.h>
+#include <linux/genetlink.h>
+#include <linux/ip_vs.h>
+
+#include <libmnl/libmnl.h>
+
+/* Fallback in case the kernel's installed uapi header is older */
+#ifndef IP_VS_SVC_F_SECURE_TCP
+#define IP_VS_SVC_F_SECURE_TCP	0x0040
+#endif
+
+/* 16-byte address storage, matching union nf_inet_addr for AF_INET */
+struct inet_addr16 {
+	uint8_t all[16];
+};
+
+/* ---------------- family resolver ---------------- */
+static int ctrl_attr_cb(const struct nlattr *attr, void *data)
+{
+	const struct nlattr **tb = data;
+	int type = mnl_attr_get_type(attr);
+
+	if (mnl_attr_type_valid(attr, CTRL_ATTR_MAX) < 0)
+		return MNL_CB_ERROR;
+	if (type == CTRL_ATTR_FAMILY_ID) {
+		if (mnl_attr_validate(attr, MNL_TYPE_U16) < 0)
+			return MNL_CB_ERROR;
+		tb[CTRL_ATTR_FAMILY_ID] = attr;
+	}
+	return MNL_CB_OK;
+}
+
+static int ctrl_data_cb(const struct nlmsghdr *nlh, void *data)
+{
+	const struct nlattr *tb[CTRL_ATTR_MAX + 1] = { 0 };
+	uint16_t *fam = data;
+
+	if (nlh->nlmsg_type != GENL_ID_CTRL)
+		return MNL_CB_OK;
+	mnl_attr_parse(nlh, sizeof(struct genlmsghdr),
+		       (mnl_attr_cb_t)ctrl_attr_cb, tb);
+	if (tb[CTRL_ATTR_FAMILY_ID]) {
+		*fam = mnl_attr_get_u16(tb[CTRL_ATTR_FAMILY_ID]);
+		return MNL_CB_STOP;
+	}
+	return MNL_CB_OK;
+}
+
+static int resolve_family(const char *name, uint16_t *fam)
+{
+	struct mnl_socket *nl;
+	char buf[MNL_SOCKET_BUFFER_SIZE];
+	struct nlmsghdr *nlh;
+	struct genlmsghdr *genl;
+	int ret;
+
+	nl = mnl_socket_open(NETLINK_GENERIC);
+	if (!nl)
+		return -errno;
+	mnl_socket_bind(nl, 0, 0);
+
+	nlh = mnl_nlmsg_put_header(buf);
+	genl = mnl_nlmsg_put_extra_header(nlh, sizeof(struct genlmsghdr));
+	genl->cmd = CTRL_CMD_GETFAMILY;
+	genl->version = 1;
+	nlh->nlmsg_type = GENL_ID_CTRL;
+	nlh->nlmsg_flags = NLM_F_REQUEST;
+	mnl_attr_put_strz(nlh, CTRL_ATTR_FAMILY_NAME, name);
+
+	if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
+		mnl_socket_close(nl);
+		return -errno;
+	}
+	do {
+		ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
+		if (ret < 0) {
+			if (errno == EAGAIN)
+				continue;
+			mnl_socket_close(nl);
+			return -errno;
+		}
+		ret = mnl_cb_run(buf, ret, 0, mnl_socket_get_portid(nl),
+				 (mnl_cb_t)ctrl_data_cb, fam);
+	} while (ret > 0 && *fam == 0);
+
+	mnl_socket_close(nl);
+	return *fam ? 0 : -ENOENT;
+}
+
+/* ---------------- fill service identifying attrs ---------------- */
+static int fill_service(struct nlmsghdr *nlh, const char *vip,
+			uint16_t port, int full, int secure)
+{
+	struct inet_addr16 vaddr = { 0 };
+	struct nlattr *nest;
+	struct ip_vs_flags fl;
+	int af = AF_INET;
+
+	if (inet_pton(af, vip, vaddr.all) != 1) {
+		fprintf(stderr, "bad VIP %s\n", vip);
+		return -EINVAL;
+	}
+
+	nest = mnl_attr_nest_start(nlh, IPVS_CMD_ATTR_SERVICE);
+	mnl_attr_put_u16(nlh, IPVS_SVC_ATTR_AF, af);
+	mnl_attr_put_u16(nlh, IPVS_SVC_ATTR_PROTOCOL, IPPROTO_TCP);
+	mnl_attr_put(nlh, IPVS_SVC_ATTR_ADDR, sizeof(vaddr), &vaddr);
+	/* port/be16: port is passed in network order from main() */
+	mnl_attr_put_u16(nlh, IPVS_SVC_ATTR_PORT, port);
+
+	if (full) {
+		mnl_attr_put_strz(nlh, IPVS_SVC_ATTR_SCHED_NAME, "rr");
+		memset(&fl, 0, sizeof(fl));
+		fl.mask = IP_VS_SVC_F_SECURE_TCP;
+		if (secure)
+			fl.flags = IP_VS_SVC_F_SECURE_TCP;
+		mnl_attr_put(nlh, IPVS_SVC_ATTR_FLAGS, sizeof(fl), &fl);
+		mnl_attr_put_u32(nlh, IPVS_SVC_ATTR_TIMEOUT, 0);
+		mnl_attr_put_u32(nlh, IPVS_SVC_ATTR_NETMASK, 0xffffffff);
+	}
+	mnl_attr_nest_end(nlh, nest);
+	return 0;
+}
+
+static int send_cmd(struct mnl_socket *nl, struct nlmsghdr *nlh)
+{
+	if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
+		perror("sendto");
+		return -1;
+	}
+	return 0;
+}
+
+/* ---------------- get secure flag ---------------- */
+static int svc_attr_cb(const struct nlattr *attr, void *data)
+{
+	const struct nlattr **tb = data;
+	int type = mnl_attr_get_type(attr);
+
+	if (mnl_attr_type_valid(attr, IPVS_SVC_ATTR_MAX) < 0)
+		return MNL_CB_ERROR;
+	tb[type] = attr;
+	return MNL_CB_OK;
+}
+
+static int get_cb(const struct nlmsghdr *nlh, void *data)
+{
+	const struct nlattr *tb[IPVS_SVC_ATTR_MAX + 1] = { 0 };
+	struct ip_vs_flags fl;
+	int *secure = data;
+	struct nlattr *nest;
+
+	mnl_attr_for_each(nest, nlh, sizeof(struct genlmsghdr)) {
+		if (mnl_attr_get_type(nest) == IPVS_CMD_ATTR_SERVICE)
+			mnl_attr_parse_nested(nest, (mnl_attr_cb_t)svc_attr_cb, tb);
+	}
+	if (tb[IPVS_SVC_ATTR_FLAGS]) {
+		memcpy(&fl, mnl_attr_get_payload(tb[IPVS_SVC_ATTR_FLAGS]),
+		       sizeof(fl));
+		*secure = !!(fl.flags & IP_VS_SVC_F_SECURE_TCP);
+	}
+	return MNL_CB_STOP;
+}
+
+static int do_get(uint16_t fam, const char *vip, uint16_t port)
+{
+	struct mnl_socket *nl;
+	char buf[MNL_SOCKET_BUFFER_SIZE];
+	struct nlmsghdr *nlh;
+	struct genlmsghdr *genl;
+	int ret, secure = -1;
+
+	nl = mnl_socket_open(NETLINK_GENERIC);
+	mnl_socket_bind(nl, 0, 0);
+	nlh = mnl_nlmsg_put_header(buf);
+	genl = mnl_nlmsg_put_extra_header(nlh, sizeof(struct genlmsghdr));
+	genl->cmd = IPVS_CMD_GET_SERVICE;
+	genl->version = IPVS_GENL_VERSION;
+	nlh->nlmsg_type = fam;
+	nlh->nlmsg_flags = NLM_F_REQUEST;
+	fill_service(nlh, vip, port, 0, 0);
+	send_cmd(nl, nlh);
+
+	ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
+	while (ret >= 0) {
+		ret = mnl_cb_run(buf, ret, 0, mnl_socket_get_portid(nl),
+				 (mnl_cb_t)get_cb, &secure);
+		if (ret <= MNL_CB_STOP || secure >= 0)
+			break;
+		ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
+	}
+	mnl_socket_close(nl);
+	if (secure < 0)
+		return -ENOENT;
+	printf("secure_tcp=%d\n", secure);
+	return 0;
+}
+
+/* ---------------- add service with flag ---------------- */
+static int do_add(uint16_t fam, const char *vip, uint16_t port, int secure)
+{
+	struct mnl_socket *nl;
+	char buf[MNL_SOCKET_BUFFER_SIZE];
+	struct nlmsghdr *nlh;
+	struct genlmsghdr *genl;
+	int ret;
+
+	/* NLM_F_EXCL: fail if the service already exists */
+	nlh = mnl_nlmsg_put_header(buf);
+	genl = mnl_nlmsg_put_extra_header(nlh, sizeof(struct genlmsghdr));
+	genl->cmd = IPVS_CMD_NEW_SERVICE;
+	genl->version = IPVS_GENL_VERSION;
+	nlh->nlmsg_type = fam;
+	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
+	fill_service(nlh, vip, port, 1, secure);
+
+	nl = mnl_socket_open(NETLINK_GENERIC);
+	mnl_socket_bind(nl, 0, 0);
+	if (send_cmd(nl, nlh) < 0) {
+		mnl_socket_close(nl);
+		return 1;
+	}
+
+	/* Read the reply so we can report why a command may have failed */
+	for (;;) {
+		ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
+		if (ret <= 0)
+			break;
+		ret = mnl_cb_run(buf, ret, 0, mnl_socket_get_portid(nl),
+				 NULL, NULL);
+		if (ret < 0) {
+			int e = errno;
+
+			fprintf(stderr, "IPVS netlink error: ret=%d errno=%d (%s)\n",
+				ret, e, strerror(e));
+			mnl_socket_close(nl);
+			return 1;
+		}
+		if (ret <= MNL_CB_STOP)
+			break;
+	}
+	mnl_socket_close(nl);
+	return 0;
+}
+
+int main(int argc, char *argv[])
+{
+	const char *cmd, *vip;
+	uint16_t fam;
+	uint16_t port;
+	int ret, secure = 0;
+
+	if (argc < 4) {
+		fprintf(stderr,
+			"usage: %s add <vip> <port> <secure|plain>\n"
+			"       %s get <vip> <port>\n", argv[0], argv[0]);
+		return 2;
+	}
+	cmd = argv[1];
+	vip = argv[2];
+	port = (uint16_t)atoi(argv[3]);
+	port = htons(port);
+
+	ret = resolve_family(IPVS_GENL_NAME, &fam);
+	if (ret) {
+		fprintf(stderr, "cannot resolve IPVS genl family: %s\n",
+			strerror(-ret));
+		return 1;
+	}
+
+	if (strcmp(cmd, "add") == 0) {
+		if (argc < 5) {
+			fprintf(stderr, "usage: %s add ... <secure|plain>\n",
+				argv[0]);
+			return 2;
+		}
+		if (strcmp(argv[4], "secure") == 0) {
+			secure = 1;
+		} else if (strcmp(argv[4], "plain") != 0) {
+			fprintf(stderr, "unknown mode %s\n", argv[4]);
+			return 2;
+		}
+		return do_add(fam, vip, port, secure);
+	} else if (strcmp(cmd, "get") == 0) {
+		return do_get(fam, vip, port);
+	}
+
+	fprintf(stderr, "unknown command %s\n", cmd);
+	return 2;
+}
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-09-12  1:32 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-12  1:32 [PATCH 0/4] ipvs: add per-service secure_tcp Adriano Cordova
2026-09-12  1:32 ` [PATCH 1/4] ipvs: add flags for per-service secure TCP state table Adriano Cordova
2026-09-12  1:32 ` [PATCH 2/4] ipvs: stamp per-service secure_tcp on new connections Adriano Cordova
2026-09-12  1:32 ` [PATCH 3/4] ipvs: tcp: enable per-connection secure_tcp in state machine Adriano Cordova
2026-09-12  1:32 ` [PATCH 4/4] selftests: netfilter: ipvs: add per-service secure_tcp test Adriano Cordova

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox