* [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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.