[PATCH] net: add option for using randomized TCP ISNs

From: Aaron Rainbolt
Date: Mon Oct 07 2024 - 22:27:53 EST


This patch is not yet ready for merging into the kernel. While there's
nothing in particular wrong with it that would keep it from being
usable in its current state (that I'm aware of), I'm conscious of the
fact that we are well past the merge window and am posting this
primarily for feedback. There's also some testing code left over to
make it easy for people to test the patch, as well as detailed
instructions on how to test it.

The current algorithm for generating TCP Initial Sequence Numbers
(ISNs) is to take data about the TCP connection, hash it with a
one-time generated key and SipHash, then add a system timer value to
it. This algorithm has the drawback of exposing relative timestamps to
servers. There are both known[1] and suspected[2] timing attacks that
can be carried out when relative timestamp values are used, and while
these may not be a concern for most, they are potentially concerning to
some subset of Linux users. Currently the only good solution for issues
who are worried about this is to use a third-party, out-of-tree kernel
module[3] that hot-patches the `secure_tcpv6_seq` and `secure_tcp_seq`
functions.

While the TCP protocol may suffer slight performance issues when ISNs
are not managed in an orderly fashion like the kernel currently does,
in practice it appears to not cause substantial issues in a desktop use
case. (I have not yet benchmarked this, but the author of [2] seems to
have not found the performance impact concerning, and whatever impact
there was, was unnoticeable for me in my limited testing.) Therefore it
is likely practical to simply use entirely random 32-bit numbers for
TCP ISNs, which is what this patch allows. Due to the possible
performance impact, this is not something that would be appropriate to
apply to all users of the Linux kernel, and so it is instead
implemented as an optional feature that can be toggled with a kernel
parameter.

To make testing easier, there are a couple of lines marked as "TODO:
Remove before merge" that print every ISN that is generated to
the kernel log. To test:

* Apply this patch to Linux 6.12-rc2, build it, and boot from it. Do
not add `tcp_rand_isn` to the kernel command line.
* Create two Python scripts, "hammer.py" and "anvil.py". The content of
these scripts should be as follows:

hammer.py:
#!/usr/bin/python3
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 4434))
s.connect(("127.0.0.1", 7899))
s.close()

anvil.py:
#!/usr/bin/python3
import socket
import time
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 7899))
s.listen(100)
while True:
conn, addr = s.accept()
data = conn.recv(1024)
print(repr(data))
time.sleep(0.001)
conn.close()

* In one terminal, run `tail -f` on your logfile (for systemd-based
distros use `journalctl -f`) to observe the kernel's notifications
about generated ISNs.
* In a second terminal, run `python3 anvil.py`. It will block.
* In a third terminal, run `while true; do python3 hammer.py; done`.
This will repeatedly establish a connection to the anvil.py server,
then disconnect. The connections will always be established from the
same source port. You should see a flood of ISN notifications in your
kernel log. The number shown should constantly increment by small
amounts.
* Reboot with the `tcp_rand_isn` kernel parameter, then run anvil.py
and hammer.py as shown above while watching the syslog. You should
see a flood of ISN notifications in your kernel log, and the number
shown should change unpredictably.

[1]: https://murdoch.is/papers/ccs06hotornot.pdf
[2]: https://bitguard.wordpress.com/2019/09/03/an-analysis-of-tcp-secure-sn-generation-in-linux-and-its-privacy-issues/
[3]: https://github.com/0xsirus/tirdad/

Signed-off-by: Aaron Rainbolt <arainbolt@xxxxxxxxxx>
---
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 1518343bbe22..26472137086c 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -6645,6 +6645,16 @@
neutralize any effect of /proc/sys/kernel/sysrq.
Useful for debugging.

+ tcp_rand_isn
+ [NET]
+ Enables randomized TCP Initial Sequence Number (ISN)
+ generation. When disabled, the kernel will use an
+ algorithm for ISN generation that provides better
+ performance at the cost of potentially leaking timing
+ information over the network. When enabled, TCP ISNs
+ will be randomly generated, providing better security
+ with a potential performance hit.
+
tcpmhash_entries= [KNL,NET]
Set the number of tcp_metrics_hash slots.
Default value is 8192 or 16384 depending on total
diff --git a/net/core/secure_seq.c b/net/core/secure_seq.c
index b0ff6153be62..9982e8fcd679 100644
--- a/net/core/secure_seq.c
+++ b/net/core/secure_seq.c
@@ -22,6 +22,14 @@
static siphash_aligned_key_t net_secret;
static siphash_aligned_key_t ts_secret;

+static bool tcp_rand_isn;
+static int __init parse_tcp_rand_isn(char *arg)
+{
+ tcp_rand_isn = true;
+ return 0;
+}
+early_param("tcp_rand_isn", parse_tcp_rand_isn);
+
#define EPHEMERAL_PORT_SHUFFLE_PERIOD (10 * HZ)

static __always_inline void net_secret_init(void)
@@ -76,23 +84,33 @@ EXPORT_SYMBOL(secure_tcpv6_ts_off);
u32 secure_tcpv6_seq(const __be32 *saddr, const __be32 *daddr,
__be16 sport, __be16 dport)
{
- const struct {
- struct in6_addr saddr;
- struct in6_addr daddr;
- __be16 sport;
- __be16 dport;
- } __aligned(SIPHASH_ALIGNMENT) combined = {
- .saddr = *(struct in6_addr *)saddr,
- .daddr = *(struct in6_addr *)daddr,
- .sport = sport,
- .dport = dport
- };
u32 hash;

- net_secret_init();
- hash = siphash(&combined, offsetofend(typeof(combined), dport),
- &net_secret);
- return seq_scale(hash);
+ if (tcp_rand_isn) {
+ get_random_bytes(((char *)&hash), sizeof(hash));
+ } else {
+ const struct {
+ struct in6_addr saddr;
+ struct in6_addr daddr;
+ __be16 sport;
+ __be16 dport;
+ } __aligned(SIPHASH_ALIGNMENT) combined = {
+ .saddr = *(struct in6_addr *)saddr,
+ .daddr = *(struct in6_addr *)daddr,
+ .sport = sport,
+ .dport = dport
+ };
+
+ net_secret_init();
+ hash = siphash(&combined, offsetofend(typeof(combined),
+ dport),
+ &net_secret);
+ hash = seq_scale(hash);
+ }
+
+ // TODO: Remove before merge
+ printk("secure_seq: tcpv6 isn: %u", hash);
+ return hash;
}
EXPORT_SYMBOL(secure_tcpv6_seq);

@@ -138,11 +156,19 @@ u32 secure_tcp_seq(__be32 saddr, __be32 daddr,
{
u32 hash;

- net_secret_init();
- hash = siphash_3u32((__force u32)saddr, (__force u32)daddr,
- (__force u32)sport << 16 | (__force u32)dport,
- &net_secret);
- return seq_scale(hash);
+ if (tcp_rand_isn) {
+ get_random_bytes(((char *)&hash), sizeof(hash));
+ } else {
+ net_secret_init();
+ hash = siphash_3u32((__force u32)saddr, (__force u32)daddr,
+ (__force u32)sport << 16 | (__force u32)dport,
+ &net_secret);
+ hash = seq_scale(hash);
+ }
+
+ // TODO: Remove before merge
+ printk("secure_seq: tcp isn: %u", hash);
+ return hash;
}
EXPORT_SYMBOL_GPL(secure_tcp_seq);