* [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(¶m, 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(¶m, 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, ¶m);
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