2021-08-24 21:36:26

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 00/15] tcp: Initial support for RFC5925 auth option

This is similar to TCP MD5 in functionality but it's sufficiently
different that wire formats are incompatible. Compared to TCP-MD5
more algorithms are supported and multiple keys can be used on the
same connection but there is still no negotiation mechanism.
Expected use-case is protecting long-duration BGP/LDP connections
between routers using pre-shared keys.

This version is mostly functional, it incorporates ABI feedback from
previous versions and adds tests to kselftests. More discussion and
testing is required and obvious optimizations were skipped in favor of
adding functionality. Here are several flaws:

* RST and TIMEWAIT are mostly unhandled
* Locking is lockdep-clean but need to be revised
* Sequence Number Extension not implemented
* User is responsible for ensuring keys do not overlap
* Traffic key is not cached (reducing performance)

Not all ABI suggestions were incorporated, they can be discussed further.
However I very much want to avoid supporting algorithms beyond RFC5926.

Test suite was added to tools/selftests/tcp_authopt. Tests are written
in python using pytest and scapy and check the API in some detail and
validate packet captures. Python code is already in linux and in
kselftests but virtualenvs not very much. This test suite uses `tox` to
create a private virtualenv and hide dependencies. Let me know if this
is OK or how it can be improved.

Limited testing support is also included in nettest and fcnal-test.sh,
those tests are slow and cover much less.

Changes for frr: https://github.com/FRRouting/frr/pull/9442
That PR was made early for ABI feedback, it has many issues.

Changes for yabgp: https://github.com/cdleonard/yabgp/commits/tcp_authopt
The patched version of yabgp can establish a BGP session protected by
TCP Authentication Option with a Cisco IOS-XR router. It old now.

Changes since RFCv2:
* Removed local_id from ABI and match on send_id/recv_id/addr
* Add all relevant out-of-tree tests to tools/testing/selftests
* Return an error instead of ignoring unknown flags, hopefully this makes
it easier to extend.
* Check sk_family before __tcp_authopt_info_get_or_create in tcp_set_authopt_key
* Use sock_owned_by_me instead of WARN_ON(!lockdep_sock_is_held(sk))
* Fix some intermediate build failures reported by kbuild robot
* Improve documentation
Link: https://lore.kernel.org/netdev/[email protected]/

Changes since RFC:
* Split into per-topic commits for ease of review. The intermediate
commits compile with a few "unused function" warnings and don't do
anything useful by themselves.
* Add ABI documention including kernel-doc on uapi
* Fix lockdep warnings from crypto by creating pools with one shash for
each cpu
* Accept short options to setsockopt by padding with zeros; this
approach allows increasing the size of the structs in the future.
* Support for aes-128-cmac-96
* Support for binding addresses to keys in a way similar to old tcp_md5
* Add support for retrieving received keyid/rnextkeyid and controling
the keyid/rnextkeyid being sent.
Link: https://lore.kernel.org/netdev/01383a8751e97ef826ef2adf93bfde3a08195a43.1626693859.git.cdleonard@gmail.com/

Leonard Crestez (15):
tcp: authopt: Initial support and key management
docs: Add user documentation for tcp_authopt
selftests: Initial tcp_authopt test module
selftests: tcp_authopt: Initial sockopt manipulation
tcp: authopt: Add crypto initialization
tcp: authopt: Compute packet signatures
tcp: authopt: Hook into tcp core
tcp: authopt: Add snmp counters
selftests: tcp_authopt: Test key address binding
selftests: tcp_authopt: Capture and verify packets
selftests: Initial tcp_authopt support for nettest
selftests: Initial tcp_authopt support for fcnal-test
selftests: Add -t tcp_authopt option for fcnal-test.sh
tcp: authopt: Add key selection controls
selftests: tcp_authopt: Add tests for rollover

Documentation/networking/index.rst | 1 +
Documentation/networking/tcp_authopt.rst | 69 +
include/linux/tcp.h | 6 +
include/net/tcp.h | 1 +
include/net/tcp_authopt.h | 134 ++
include/uapi/linux/snmp.h | 1 +
include/uapi/linux/tcp.h | 110 ++
net/ipv4/Kconfig | 14 +
net/ipv4/Makefile | 1 +
net/ipv4/proc.c | 1 +
net/ipv4/tcp.c | 27 +
net/ipv4/tcp_authopt.c | 1168 +++++++++++++++++
net/ipv4/tcp_input.c | 17 +
net/ipv4/tcp_ipv4.c | 5 +
net/ipv4/tcp_minisocks.c | 2 +
net/ipv4/tcp_output.c | 74 +-
net/ipv6/tcp_ipv6.c | 4 +
tools/testing/selftests/net/fcnal-test.sh | 34 +
tools/testing/selftests/net/nettest.c | 34 +-
tools/testing/selftests/tcp_authopt/Makefile | 5 +
.../testing/selftests/tcp_authopt/README.rst | 15 +
tools/testing/selftests/tcp_authopt/config | 6 +
tools/testing/selftests/tcp_authopt/run.sh | 11 +
tools/testing/selftests/tcp_authopt/setup.cfg | 17 +
tools/testing/selftests/tcp_authopt/setup.py | 5 +
.../tcp_authopt/tcp_authopt_test/__init__.py | 0
.../tcp_authopt/tcp_authopt_test/conftest.py | 21 +
.../full_tcp_sniff_session.py | 53 +
.../tcp_authopt_test/linux_tcp_authopt.py | 198 +++
.../tcp_authopt_test/netns_fixture.py | 63 +
.../tcp_authopt/tcp_authopt_test/server.py | 82 ++
.../tcp_authopt/tcp_authopt_test/sockaddr.py | 101 ++
.../tcp_authopt_test/tcp_authopt_alg.py | 276 ++++
.../tcp_authopt/tcp_authopt_test/test_bind.py | 143 ++
.../tcp_authopt_test/test_rollover.py | 181 +++
.../tcp_authopt_test/test_sockopt.py | 74 ++
.../tcp_authopt_test/test_vectors.py | 359 +++++
.../tcp_authopt_test/test_verify_capture.py | 123 ++
.../tcp_authopt/tcp_authopt_test/utils.py | 154 +++
.../tcp_authopt/tcp_authopt_test/validator.py | 158 +++
40 files changed, 3746 insertions(+), 2 deletions(-)
create mode 100644 Documentation/networking/tcp_authopt.rst
create mode 100644 include/net/tcp_authopt.h
create mode 100644 net/ipv4/tcp_authopt.c
create mode 100644 tools/testing/selftests/tcp_authopt/Makefile
create mode 100644 tools/testing/selftests/tcp_authopt/README.rst
create mode 100644 tools/testing/selftests/tcp_authopt/config
create mode 100755 tools/testing/selftests/tcp_authopt/run.sh
create mode 100644 tools/testing/selftests/tcp_authopt/setup.cfg
create mode 100644 tools/testing/selftests/tcp_authopt/setup.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/__init__.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/conftest.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/full_tcp_sniff_session.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/linux_tcp_authopt.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/netns_fixture.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/server.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/sockaddr.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/tcp_authopt_alg.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_bind.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_rollover.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_sockopt.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_vectors.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_verify_capture.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/utils.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/validator.py


base-commit: 3a62c333497b164868fdcd241842a1dd4e331825
--
2.25.1


2021-08-24 21:36:29

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 10/15] selftests: tcp_authopt: Capture and verify packets

Tools like tcpdump and wireshark can parse the TCP Authentication Option
but there is not yet support to verify correct signatures.

This patch implements TCP-AO signature verification using scapy and the
python cryptography package and applies it to captures of linux traffic
in multiple scenarios (ipv4, ipv6 etc).

The python code is verified itself with a subset of IETF test vectors
from this page:
https://datatracker.ietf.org/doc/html/draft-touch-tcpm-ao-test-vectors-02

Signed-off-by: Leonard Crestez <[email protected]>
---
.../full_tcp_sniff_session.py | 53 +++
.../tcp_authopt_test/tcp_authopt_alg.py | 276 ++++++++++++++
.../tcp_authopt_test/test_vectors.py | 359 ++++++++++++++++++
.../tcp_authopt_test/test_verify_capture.py | 123 ++++++
.../tcp_authopt/tcp_authopt_test/validator.py | 158 ++++++++
5 files changed, 969 insertions(+)
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/full_tcp_sniff_session.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/tcp_authopt_alg.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_vectors.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_verify_capture.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/validator.py

diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/full_tcp_sniff_session.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/full_tcp_sniff_session.py
new file mode 100644
index 000000000000..d709e83c8700
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/full_tcp_sniff_session.py
@@ -0,0 +1,53 @@
+# SPDX-License-Identifier: GPL-2.0
+import scapy.sessions
+from scapy.layers.inet import TCP
+
+from .utils import SimpleWaitEvent
+
+
+class FullTCPSniffSession(scapy.sessions.DefaultSession):
+ """Implementation of a scapy sniff session that can wait for a full TCP capture
+
+ Allows another thread to wait for a complete FIN handshake without polling or sleep.
+ """
+
+ found_syn = False
+ found_synack = False
+ found_fin = False
+ found_client_fin = False
+ found_server_fin = False
+
+ def __init__(self, server_port=None, **kw):
+ super().__init__(**kw)
+ self.server_port = server_port
+ self._close_event = SimpleWaitEvent()
+
+ def on_packet_received(self, p):
+ super().on_packet_received(p)
+ if not p or not TCP in p:
+ return
+ th = p[TCP]
+ # logger.debug("sport=%d dport=%d flags=%s", th.sport, th.dport, th.flags)
+ if th.flags.S and not th.flags.A:
+ if th.dport == self.server_port or self.server_port is None:
+ self.found_syn = True
+ if th.flags.S and th.flags.A:
+ if th.sport == self.server_port or self.server_port is None:
+ self.found_synack = True
+ if th.flags.F:
+ if self.server_port is None:
+ self.found_fin = True
+ self._close_event.set()
+ elif self.server_port == th.dport:
+ self.found_client_fin = True
+ self.found_fin = True
+ if self.found_server_fin and self.found_client_fin:
+ self._close_event.set()
+ elif self.server_port == th.sport:
+ self.found_server_fin = True
+ self.found_fin = True
+ if self.found_server_fin and self.found_client_fin:
+ self._close_event.set()
+
+ def wait_close(self, timeout=10):
+ self._close_event.wait(timeout=timeout)
diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/tcp_authopt_alg.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/tcp_authopt_alg.py
new file mode 100644
index 000000000000..093cb4716184
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/tcp_authopt_alg.py
@@ -0,0 +1,276 @@
+# SPDX-License-Identifier: GPL-2.0
+"""Packet-processing utilities implementing RFC5925 and RFC2926"""
+
+import logging
+from dataclasses import dataclass
+from ipaddress import IPv4Address, IPv6Address
+from scapy.layers.inet import IP, TCP
+from scapy.layers.inet6 import IPv6
+from scapy.packet import Packet
+import socket
+import struct
+import typing
+import hmac
+
+logger = logging.getLogger(__name__)
+
+
+def kdf_sha1(master_key: bytes, context: bytes) -> bytes:
+ """RFC5926 section 3.1.1.1"""
+ input = b"\x01" + b"TCP-AO" + context + b"\x00\xa0"
+ return hmac.digest(master_key, input, "SHA1")
+
+
+def mac_sha1(traffic_key: bytes, message: bytes) -> bytes:
+ """RFC5926 section 3.2.1"""
+ return hmac.digest(traffic_key, message, "SHA1")[:12]
+
+
+def cmac_aes_digest(key: bytes, msg: bytes) -> bytes:
+ from cryptography.hazmat.primitives import cmac
+ from cryptography.hazmat.primitives.ciphers import algorithms
+ from cryptography.hazmat.backends import default_backend
+
+ backend = default_backend()
+ c = cmac.CMAC(algorithms.AES(key), backend=backend)
+ c.update(bytes(msg))
+ return c.finalize()
+
+
+def kdf_cmac_aes(master_key: bytes, context: bytes) -> bytes:
+ if len(master_key) == 16:
+ key = master_key
+ else:
+ key = cmac_aes_digest(b"\x00" * 16, master_key)
+ return cmac_aes_digest(key, b"\x01" + b"TCP-AO" + context + b"\x00\x80")
+
+
+def mac_cmac_aes(traffic_key: bytes, message: bytes) -> bytes:
+ return cmac_aes_digest(traffic_key, message)[:12]
+
+
+class TcpAuthOptAlg:
+ def kdf(self, master_key: bytes, context: bytes) -> bytes:
+ raise NotImplementedError()
+
+ def mac(self, traffic_key: bytes, message: bytes) -> bytes:
+ raise NotImplementedError()
+
+
+class TcpAuthOptAlg_HMAC_SHA1(TcpAuthOptAlg):
+ def kdf(self, master_key: bytes, context: bytes) -> bytes:
+ return kdf_sha1(master_key, context)
+
+ def mac(self, traffic_key: bytes, message: bytes) -> bytes:
+ return mac_sha1(traffic_key, message)
+
+
+class TcpAuthOptAlg_CMAC_AES(TcpAuthOptAlg):
+ def kdf(self, master_key: bytes, context: bytes) -> bytes:
+ return kdf_cmac_aes(master_key, context)
+
+ def mac(self, traffic_key: bytes, message: bytes) -> bytes:
+ return mac_cmac_aes(traffic_key, message)
+
+
+def get_alg(name) -> TcpAuthOptAlg:
+ if name.upper() == "HMAC-SHA-1-96":
+ return TcpAuthOptAlg_HMAC_SHA1()
+ elif name.upper() == "AES-128-CMAC-96":
+ return TcpAuthOptAlg_CMAC_AES()
+ else:
+ raise ValueError(f"Bad TCP AuthOpt algorithms {name}")
+
+
+IPvXAddress = typing.Union[IPv4Address, IPv6Address]
+
+
+def get_scapy_ipvx_src(p: Packet) -> IPvXAddress:
+ if IP in p:
+ return IPv4Address(p[IP].src)
+ elif IPv6 in p:
+ return IPv6Address(p[IPv6].src)
+ else:
+ raise Exception("Neither IP nor IPv6 found on packet")
+
+
+def get_scapy_ipvx_dst(p: Packet) -> IPvXAddress:
+ if IP in p:
+ return IPv4Address(p[IP].dst)
+ elif IPv6 in p:
+ return IPv6Address(p[IPv6].dst)
+ else:
+ raise Exception("Neither IP nor IPv6 found on packet")
+
+
+def build_context(
+ saddr: IPvXAddress, daddr: IPvXAddress, sport, dport, src_isn, dst_isn
+) -> bytes:
+ """Build context bytes as specified by RFC5925 section 5.2"""
+ return (
+ saddr.packed
+ + daddr.packed
+ + struct.pack(
+ "!HHII",
+ sport,
+ dport,
+ src_isn,
+ dst_isn,
+ )
+ )
+
+
+def build_context_from_scapy(p: Packet, src_isn: int, dst_isn: int) -> bytes:
+ """Build context based on a scapy Packet and src/dst initial-sequence numbers"""
+ return build_context(
+ get_scapy_ipvx_src(p),
+ get_scapy_ipvx_dst(p),
+ p[TCP].sport,
+ p[TCP].dport,
+ src_isn,
+ dst_isn,
+ )
+
+
+def build_context_from_scapy_syn(p: Packet) -> bytes:
+ """Build context for a scapy SYN packet"""
+ return build_context_from_scapy(p, p[TCP].seq, 0)
+
+
+def build_context_from_scapy_synack(p: Packet) -> bytes:
+ """Build context for a scapy SYN/ACK packet"""
+ return build_context_from_scapy(p, p[TCP].seq, p[TCP].ack - 1)
+
+
+def build_message_from_scapy(p: Packet, include_options=True, sne=0) -> bytearray:
+ """Build message bytes as described by RFC5925 section 5.1"""
+ result = bytearray()
+ result += struct.pack("!I", sne)
+ # ip pseudo-header:
+ if IP in p:
+ result += struct.pack(
+ "!4s4sHH",
+ IPv4Address(p[IP].src).packed,
+ IPv4Address(p[IP].dst).packed,
+ socket.IPPROTO_TCP,
+ p[TCP].dataofs * 4 + len(p[TCP].payload),
+ )
+ assert p[TCP].dataofs * 4 + len(p[TCP].payload) + p[IP].ihl * 4 == p[IP].len
+ elif IPv6 in p:
+ result += struct.pack(
+ "!16s16sII",
+ IPv6Address(p[IPv6].src).packed,
+ IPv6Address(p[IPv6].dst).packed,
+ p[IPv6].plen,
+ socket.IPPROTO_TCP,
+ )
+ assert p[TCP].dataofs * 4 + len(p[TCP].payload) == p[IPv6].plen
+ else:
+ raise Exception("Neither IP nor IPv6 found on packet")
+
+ # tcp header with checksum set to zero
+ th_bytes = bytes(p[TCP])
+ result += th_bytes[:16]
+ result += b"\x00\x00"
+ result += th_bytes[18:20]
+
+ # Even if include_options=False the TCP-AO option itself is still included
+ # with the MAC set to all-zeros. This means we need to parse TCP options.
+ pos = 20
+ tcphdr_optend = p[TCP].dataofs * 4
+ # logger.info("th_bytes: %s", th_bytes.hex(' '))
+ assert len(th_bytes) >= tcphdr_optend
+ while pos < tcphdr_optend:
+ optnum = th_bytes[pos]
+ pos += 1
+ if optnum == 0 or optnum == 1:
+ if include_options:
+ result += bytes([optnum])
+ continue
+
+ optlen = th_bytes[pos]
+ pos += 1
+ if pos + optlen - 2 > tcphdr_optend:
+ logger.info(
+ "bad tcp option %d optlen %d beyond end-of-header", optnum, optlen
+ )
+ break
+ if optlen < 2:
+ logger.info("bad tcp option %d optlen %d less than two", optnum, optlen)
+ break
+ if optnum == 29:
+ if optlen < 4:
+ logger.info("bad tcp option %d optlen %d", optnum, optlen)
+ break
+ result += bytes([optnum, optlen])
+ result += th_bytes[pos : pos + 2]
+ result += (optlen - 4) * b"\x00"
+ elif include_options:
+ result += bytes([optnum, optlen])
+ result += th_bytes[pos : pos + optlen - 2]
+ pos += optlen - 2
+ result += bytes(p[TCP].payload)
+ return result
+
+
+@dataclass
+class TCPAuthContext:
+ """Context used to TCP Authentication option as defined in RFC5925 5.2"""
+
+ saddr: IPvXAddress = None
+ daddr: IPvXAddress = None
+ sport: int = 0
+ dport: int = 0
+ sisn: int = 0
+ disn: int = 0
+
+ def pack(self, syn=False, rev=False) -> bytes:
+ if rev:
+ return build_context(
+ self.daddr,
+ self.saddr,
+ self.dport,
+ self.sport,
+ self.disn if not syn else 0,
+ self.sisn,
+ )
+ else:
+ return build_context(
+ self.saddr,
+ self.daddr,
+ self.sport,
+ self.dport,
+ self.sisn,
+ self.disn if not syn else 0,
+ )
+
+ def rev(self) -> "TCPAuthContext":
+ """Reverse"""
+ return TCPAuthContext(
+ saddr=self.daddr,
+ daddr=self.saddr,
+ sport=self.dport,
+ dport=self.sport,
+ sisn=self.disn,
+ disn=self.sisn,
+ )
+
+ def init_from_syn_packet(self, p):
+ """Init from a SYN packet (and set dist to zero)"""
+ assert p[TCP].flags.S and not p[TCP].flags.A and p[TCP].ack == 0
+ self.saddr = get_scapy_ipvx_src(p)
+ self.daddr = get_scapy_ipvx_dst(p)
+ self.sport = p[TCP].sport
+ self.dport = p[TCP].dport
+ self.sisn = p[TCP].seq
+ self.disn = 0
+
+ def update_from_synack_packet(self, p):
+ """Update disn and check everything else matches"""
+ assert p[TCP].flags.S and p[TCP].flags.A
+ assert self.saddr == get_scapy_ipvx_dst(p)
+ assert self.daddr == get_scapy_ipvx_src(p)
+ assert self.sport == p[TCP].dport
+ assert self.dport == p[TCP].sport
+ assert self.sisn == p[TCP].ack - 1
+ self.disn = p[TCP].seq
diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_vectors.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_vectors.py
new file mode 100644
index 000000000000..f622bcf0dcbc
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_vectors.py
@@ -0,0 +1,359 @@
+# SPDX-License-Identifier: GPL-2.0
+import logging
+from ipaddress import IPv4Address, IPv6Address
+from scapy.layers.inet import IP, TCP
+from scapy.layers.inet6 import IPv6
+from .tcp_authopt_alg import get_alg, build_context_from_scapy, build_message_from_scapy
+from .utils import scapy_tcp_get_authopt_val, tcphdr_authopt
+import socket
+
+logger = logging.getLogger(__name__)
+
+
+class TestIETFVectors:
+ """Test python implementation of TCP-AO algorithms
+
+ Data is a subset of IETF test vectors:
+ https://datatracker.ietf.org/doc/html/draft-touch-tcpm-ao-test-vectors-02
+ """
+
+ master_key = b"testvector"
+ client_keyid = 61
+ server_keyid = 84
+ client_ipv4 = IPv4Address("10.11.12.13")
+ client_ipv6 = IPv6Address("FD00::1")
+ server_ipv4 = IPv4Address("172.27.28.29")
+ server_ipv6 = IPv6Address("FD00::2")
+
+ client_isn_41x = 0xFBFBAB5A
+ server_isn_41x = 0x11C14261
+ client_isn_42x = 0xCB0EFBEE
+ server_isn_42x = 0xACD5B5E1
+ client_isn_61x = 0x176A833F
+ server_isn_61x = 0x3F51994B
+ client_isn_62x = 0x020C1E69
+ server_isn_62x = 0xEBA3734D
+
+ def check(
+ self,
+ packet_hex: str,
+ traffic_key_hex: str,
+ mac_hex: str,
+ src_isn,
+ dst_isn,
+ include_options=True,
+ alg_name="HMAC-SHA-1-96",
+ sne=0,
+ ):
+ packet_bytes = bytes.fromhex(packet_hex)
+
+ # sanity check for ip version
+ ipv = packet_bytes[0] >> 4
+ if ipv == 4:
+ p = IP(bytes.fromhex(packet_hex))
+ assert p[IP].proto == socket.IPPROTO_TCP
+ elif ipv == 6:
+ p = IPv6(bytes.fromhex(packet_hex))
+ assert p[IPv6].nh == socket.IPPROTO_TCP
+ else:
+ raise ValueError(f"bad ipv={ipv}")
+
+ # sanity check for seq/ack in SYN/ACK packets
+ if p[TCP].flags.S and p[TCP].flags.A is False:
+ assert p[TCP].seq == src_isn
+ assert p[TCP].ack == 0
+ if p[TCP].flags.S and p[TCP].flags.A:
+ assert p[TCP].seq == src_isn
+ assert p[TCP].ack == dst_isn + 1
+
+ # check traffic key
+ alg = get_alg(alg_name)
+ context_bytes = build_context_from_scapy(p, src_isn, dst_isn)
+ traffic_key = alg.kdf(self.master_key, context_bytes)
+ assert traffic_key.hex(" ") == traffic_key_hex
+
+ # check mac
+ message_bytes = build_message_from_scapy(
+ p, include_options=include_options, sne=sne
+ )
+ mac = alg.mac(traffic_key, message_bytes)
+ assert mac.hex(" ") == mac_hex
+
+ # check option bytes in header
+ opt = scapy_tcp_get_authopt_val(p[TCP])
+ assert opt is not None
+ assert opt.keyid in [self.client_keyid, self.server_keyid]
+ assert opt.rnextkeyid in [self.client_keyid, self.server_keyid]
+ assert opt.mac.hex(" ") == mac_hex
+
+ def test_4_1_1(self):
+ self.check(
+ """
+ 45 e0 00 4c dd 0f 40 00 ff 06 bf 6b 0a 0b 0c 0d
+ ac 1b 1c 1d e9 d7 00 b3 fb fb ab 5a 00 00 00 00
+ e0 02 ff ff ca c4 00 00 02 04 05 b4 01 03 03 08
+ 04 02 08 0a 00 15 5a b7 00 00 00 00 1d 10 3d 54
+ 2e e4 37 c6 f8 ed e6 d7 c4 d6 02 e7
+ """,
+ "6d 63 ef 1b 02 fe 15 09 d4 b1 40 27 07 fd 7b 04 16 ab b7 4f",
+ "2e e4 37 c6 f8 ed e6 d7 c4 d6 02 e7",
+ self.client_isn_41x,
+ 0,
+ )
+
+ def test_4_1_2(self):
+ self.check(
+ """
+ 45 e0 00 4c 65 06 40 00 ff 06 37 75 ac 1b 1c 1d
+ 0a 0b 0c 0d 00 b3 e9 d7 11 c1 42 61 fb fb ab 5b
+ e0 12 ff ff 37 76 00 00 02 04 05 b4 01 03 03 08
+ 04 02 08 0a 84 a5 0b eb 00 15 5a b7 1d 10 54 3d
+ ee ab 0f e2 4c 30 10 81 51 16 b3 be
+ """,
+ "d9 e2 17 e4 83 4a 80 ca 2f 3f d8 de 2e 41 b8 e6 79 7f ea 96",
+ "ee ab 0f e2 4c 30 10 81 51 16 b3 be",
+ self.server_isn_41x,
+ self.client_isn_41x,
+ )
+
+ def test_4_1_3(self):
+ self.check(
+ """
+ 45 e0 00 87 36 a1 40 00 ff 06 65 9f 0a 0b 0c 0d
+ ac 1b 1c 1d e9 d7 00 b3 fb fb ab 5b 11 c1 42 62
+ c0 18 01 04 a1 62 00 00 01 01 08 0a 00 15 5a c1
+ 84 a5 0b eb 1d 10 3d 54 70 64 cf 99 8c c6 c3 15
+ c2 c2 e2 bf ff ff ff ff ff ff ff ff ff ff ff ff
+ ff ff ff ff 00 43 01 04 da bf 00 b4 0a 0b 0c 0d
+ 26 02 06 01 04 00 01 00 01 02 02 80 00 02 02 02
+ 00 02 02 42 00 02 06 41 04 00 00 da bf 02 08 40
+ 06 00 64 00 01 01 00
+ """,
+ "d2 e5 9c 65 ff c7 b1 a3 93 47 65 64 63 b7 0e dc 24 a1 3d 71",
+ "70 64 cf 99 8c c6 c3 15 c2 c2 e2 bf",
+ self.client_isn_41x,
+ self.server_isn_41x,
+ )
+
+ def test_4_1_4(self):
+ self.check(
+ """
+ 45 e0 00 87 1f a9 40 00 ff 06 7c 97 ac 1b 1c 1d
+ 0a 0b 0c 0d 00 b3 e9 d7 11 c1 42 62 fb fb ab 9e
+ c0 18 01 00 40 0c 00 00 01 01 08 0a 84 a5 0b f5
+ 00 15 5a c1 1d 10 54 3d a6 3f 0e cb bb 2e 63 5c
+ 95 4d ea c7 ff ff ff ff ff ff ff ff ff ff ff ff
+ ff ff ff ff 00 43 01 04 da c0 00 b4 ac 1b 1c 1d
+ 26 02 06 01 04 00 01 00 01 02 02 80 00 02 02 02
+ 00 02 02 42 00 02 06 41 04 00 00 da c0 02 08 40
+ 06 00 64 00 01 01 00
+ """,
+ "d9 e2 17 e4 83 4a 80 ca 2f 3f d8 de 2e 41 b8 e6 79 7f ea 96",
+ "a6 3f 0e cb bb 2e 63 5c 95 4d ea c7",
+ self.server_isn_41x,
+ self.client_isn_41x,
+ )
+
+ def test_4_2_1(self):
+ self.check(
+ """
+ 45 e0 00 4c 53 99 40 00 ff 06 48 e2 0a 0b 0c 0d
+ ac 1b 1c 1d ff 12 00 b3 cb 0e fb ee 00 00 00 00
+ e0 02 ff ff 54 1f 00 00 02 04 05 b4 01 03 03 08
+ 04 02 08 0a 00 02 4c ce 00 00 00 00 1d 10 3d 54
+ 80 af 3c fe b8 53 68 93 7b 8f 9e c2
+ """,
+ "30 ea a1 56 0c f0 be 57 da b5 c0 45 22 9f b1 0a 42 3c d7 ea",
+ "80 af 3c fe b8 53 68 93 7b 8f 9e c2",
+ self.client_isn_42x,
+ 0,
+ include_options=False,
+ )
+
+ def test_4_2_2(self):
+ self.check(
+ """
+ 45 e0 00 4c 32 84 40 00 ff 06 69 f7 ac 1b 1c 1d
+ 0a 0b 0c 0d 00 b3 ff 12 ac d5 b5 e1 cb 0e fb ef
+ e0 12 ff ff 38 8e 00 00 02 04 05 b4 01 03 03 08
+ 04 02 08 0a 57 67 72 f3 00 02 4c ce 1d 10 54 3d
+ 09 30 6f 9a ce a6 3a 8c 68 cb 9a 70
+ """,
+ "b5 b2 89 6b b3 66 4e 81 76 b0 ed c6 e7 99 52 41 01 a8 30 7f",
+ "09 30 6f 9a ce a6 3a 8c 68 cb 9a 70",
+ self.server_isn_42x,
+ self.client_isn_42x,
+ include_options=False,
+ )
+
+ def test_4_2_3(self):
+ self.check(
+ """
+ 45 e0 00 87 a8 f5 40 00 ff 06 f3 4a 0a 0b 0c 0d
+ ac 1b 1c 1d ff 12 00 b3 cb 0e fb ef ac d5 b5 e2
+ c0 18 01 04 6c 45 00 00 01 01 08 0a 00 02 4c ce
+ 57 67 72 f3 1d 10 3d 54 71 06 08 cc 69 6c 03 a2
+ 71 c9 3a a5 ff ff ff ff ff ff ff ff ff ff ff ff
+ ff ff ff ff 00 43 01 04 da bf 00 b4 0a 0b 0c 0d
+ 26 02 06 01 04 00 01 00 01 02 02 80 00 02 02 02
+ 00 02 02 42 00 02 06 41 04 00 00 da bf 02 08 40
+ 06 00 64 00 01 01 00
+ """,
+ "f3 db 17 93 d7 91 0e cd 80 6c 34 f1 55 ea 1f 00 34 59 53 e3",
+ "71 06 08 cc 69 6c 03 a2 71 c9 3a a5",
+ self.client_isn_42x,
+ self.server_isn_42x,
+ include_options=False,
+ )
+
+ def test_4_2_4(self):
+ self.check(
+ """
+ 45 e0 00 87 54 37 40 00 ff 06 48 09 ac 1b 1c 1d
+ 0a 0b 0c 0d 00 b3 ff 12 ac d5 b5 e2 cb 0e fc 32
+ c0 18 01 00 46 b6 00 00 01 01 08 0a 57 67 72 f3
+ 00 02 4c ce 1d 10 54 3d 97 76 6e 48 ac 26 2d e9
+ ae 61 b4 f9 ff ff ff ff ff ff ff ff ff ff ff ff
+ ff ff ff ff 00 43 01 04 da c0 00 b4 ac 1b 1c 1d
+ 26 02 06 01 04 00 01 00 01 02 02 80 00 02 02 02
+ 00 02 02 42 00 02 06 41 04 00 00 da c0 02 08 40
+ 06 00 64 00 01 01 00
+ """,
+ "b5 b2 89 6b b3 66 4e 81 76 b0 ed c6 e7 99 52 41 01 a8 30 7f",
+ "97 76 6e 48 ac 26 2d e9 ae 61 b4 f9",
+ self.server_isn_42x,
+ self.client_isn_42x,
+ include_options=False,
+ )
+
+ def test_5_1_1(self):
+ self.check(
+ """
+ 45 e0 00 4c 7b 9f 40 00 ff 06 20 dc 0a 0b 0c 0d
+ ac 1b 1c 1d c4 fa 00 b3 78 7a 1d df 00 00 00 00
+ e0 02 ff ff 5a 0f 00 00 02 04 05 b4 01 03 03 08
+ 04 02 08 0a 00 01 7e d0 00 00 00 00 1d 10 3d 54
+ e4 77 e9 9c 80 40 76 54 98 e5 50 91
+ """,
+ "f5 b8 b3 d5 f3 4f db b6 eb 8d 4a b9 66 0e 60 e3",
+ "e4 77 e9 9c 80 40 76 54 98 e5 50 91",
+ 0x787A1DDF,
+ 0,
+ include_options=True,
+ alg_name="AES-128-CMAC-96",
+ )
+
+ def test_6_1_1(self):
+ self.check(
+ """
+ 6e 08 91 dc 00 38 06 40 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 01 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 02 f7 e4 00 b3 17 6a 83 3f
+ 00 00 00 00 e0 02 ff ff 47 21 00 00 02 04 05 a0
+ 01 03 03 08 04 02 08 0a 00 41 d0 87 00 00 00 00
+ 1d 10 3d 54 90 33 ec 3d 73 34 b6 4c 5e dd 03 9f
+ """,
+ "62 5e c0 9d 57 58 36 ed c9 b6 42 84 18 bb f0 69 89 a3 61 bb",
+ "90 33 ec 3d 73 34 b6 4c 5e dd 03 9f",
+ self.client_isn_61x,
+ 0,
+ include_options=True,
+ )
+
+ def test_6_1_2(self):
+ self.check(
+ """
+ 6e 01 00 9e 00 38 06 40 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 01 00 b3 f7 e4 3f 51 99 4b
+ 17 6a 83 40 e0 12 ff ff bf ec 00 00 02 04 05 a0
+ 01 03 03 08 04 02 08 0a bd 33 12 9b 00 41 d0 87
+ 1d 10 54 3d f1 cb a3 46 c3 52 61 63 f7 1f 1f 55
+ """,
+ "e4 a3 7a da 2a 0a fc a8 71 14 34 91 3f e1 38 c7 71 eb cb 4a",
+ "f1 cb a3 46 c3 52 61 63 f7 1f 1f 55",
+ self.server_isn_61x,
+ self.client_isn_61x,
+ include_options=True,
+ )
+
+ def test_6_2_2(self):
+ self.check(
+ """
+ 6e 0a 7e 1f 00 38 06 40 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 01 00 b3 c6 cd eb a3 73 4d
+ 02 0c 1e 6a e0 12 ff ff 77 4d 00 00 02 04 05 a0
+ 01 03 03 08 04 02 08 0a 5e c9 9b 70 00 9d b9 5b
+ 1d 10 54 3d 3c 54 6b ad 97 43 f1 2d f8 b8 01 0d
+ """,
+ "40 51 08 94 7f 99 65 75 e7 bd bc 26 d4 02 16 a2 c7 fa 91 bd",
+ "3c 54 6b ad 97 43 f1 2d f8 b8 01 0d",
+ self.server_isn_62x,
+ self.client_isn_62x,
+ include_options=False,
+ )
+
+ def test_6_2_4(self):
+ self.check(
+ """
+ 6e 0a 7e 1f 00 73 06 40 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 01 00 b3 c6 cd eb a3 73 4e
+ 02 0c 1e ad c0 18 01 00 71 6a 00 00 01 01 08 0a
+ 5e c9 9b 7a 00 9d b9 65 1d 10 54 3d 55 9a 81 94
+ 45 b4 fd e9 8d 9e 13 17 ff ff ff ff ff ff ff ff
+ ff ff ff ff ff ff ff ff 00 43 01 04 fd e8 00 b4
+ 01 01 01 7a 26 02 06 01 04 00 01 00 01 02 02 80
+ 00 02 02 02 00 02 02 42 00 02 06 41 04 00 00 fd
+ e8 02 08 40 06 00 64 00 01 01 00
+ """,
+ "40 51 08 94 7f 99 65 75 e7 bd bc 26 d4 02 16 a2 c7 fa 91 bd",
+ "55 9a 81 94 45 b4 fd e9 8d 9e 13 17",
+ self.server_isn_62x,
+ self.client_isn_62x,
+ include_options=False,
+ )
+
+ server_isn_71x = 0xA6744ECB
+ client_isn_71x = 0x193CCCEC
+
+ def test_7_1_2(self):
+ self.check(
+ """
+ 6e 06 15 20 00 38 06 40 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 01 00 b3 f8 5a a6 74 4e cb
+ 19 3c cc ed e0 12 ff ff ea bb 00 00 02 04 05 a0
+ 01 03 03 08 04 02 08 0a 71 da ab c8 13 e4 ab 99
+ 1d 10 54 3d dc 28 43 a8 4e 78 a6 bc fd c5 ed 80
+ """,
+ "cf 1b 1e 22 5e 06 a6 36 16 76 4a 06 7b 46 f4 b1",
+ "dc 28 43 a8 4e 78 a6 bc fd c5 ed 80",
+ self.server_isn_71x,
+ self.client_isn_71x,
+ alg_name="AES-128-CMAC-96",
+ include_options=True,
+ )
+
+ def test_7_1_4(self):
+ self.check(
+ """
+ 6e 06 15 20 00 73 06 40 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 02 fd 00 00 00 00 00 00 00
+ 00 00 00 00 00 00 00 01 00 b3 f8 5a a6 74 4e cc
+ 19 3c cd 30 c0 18 01 00 52 f4 00 00 01 01 08 0a
+ 71 da ab d3 13 e4 ab a3 1d 10 54 3d c1 06 9b 7d
+ fd 3d 69 3a 6d f3 f2 89 ff ff ff ff ff ff ff ff
+ ff ff ff ff ff ff ff ff 00 43 01 04 fd e8 00 b4
+ 01 01 01 7a 26 02 06 01 04 00 01 00 01 02 02 80
+ 00 02 02 02 00 02 02 42 00 02 06 41 04 00 00 fd
+ e8 02 08 40 06 00 64 00 01 01 00
+ """,
+ "cf 1b 1e 22 5e 06 a6 36 16 76 4a 06 7b 46 f4 b1",
+ "c1 06 9b 7d fd 3d 69 3a 6d f3 f2 89",
+ self.server_isn_71x,
+ self.client_isn_71x,
+ alg_name="AES-128-CMAC-96",
+ include_options=True,
+ )
diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_verify_capture.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_verify_capture.py
new file mode 100644
index 000000000000..1bc0f05197b8
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_verify_capture.py
@@ -0,0 +1,123 @@
+# SPDX-License-Identifier: GPL-2.0
+"""Capture packets with TCP-AO and verify signatures"""
+
+import logging
+import os
+import socket
+
+import pytest
+
+from . import linux_tcp_authopt, tcp_authopt_alg
+from .full_tcp_sniff_session import FullTCPSniffSession
+from .linux_tcp_authopt import (
+ set_tcp_authopt_key,
+ tcp_authopt_key,
+)
+from .server import SimpleServerThread
+from .utils import (
+ DEFAULT_TCP_SERVER_PORT,
+ AsyncSnifferContext,
+ check_socket_echo,
+ create_listen_socket,
+ nstat_json,
+)
+from .validator import TcpAuthValidator, TcpAuthValidatorKey
+from .conftest import skipif_missing_tcp_authopt
+
+logger = logging.getLogger(__name__)
+pytestmark = skipif_missing_tcp_authopt
+
+
+def can_capture():
+ # This is too restrictive:
+ return os.geteuid() == 0
+
+
+skipif_cant_capture = pytest.mark.skipif(
+ not can_capture(), reason="run as root to capture packets"
+)
+
+
+def get_alg_id(alg_name) -> int:
+ if alg_name == "HMAC-SHA-1-96":
+ return linux_tcp_authopt.TCP_AUTHOPT_ALG_HMAC_SHA_1_96
+ elif alg_name == "AES-128-CMAC-96":
+ return linux_tcp_authopt.TCP_AUTHOPT_ALG_AES_128_CMAC_96
+ else:
+ raise ValueError()
+
+
+@skipif_cant_capture
[email protected](
+ "address_family,alg_name,include_options",
+ [
+ (socket.AF_INET, "HMAC-SHA-1-96", True),
+ (socket.AF_INET, "AES-128-CMAC-96", True),
+ (socket.AF_INET, "AES-128-CMAC-96", False),
+ (socket.AF_INET6, "HMAC-SHA-1-96", True),
+ (socket.AF_INET6, "HMAC-SHA-1-96", False),
+ (socket.AF_INET6, "AES-128-CMAC-96", True),
+ ],
+)
+def test_verify_capture(exit_stack, address_family, alg_name, include_options):
+ master_key = b"testvector"
+ alg_id = get_alg_id(alg_name)
+
+ session = FullTCPSniffSession(server_port=DEFAULT_TCP_SERVER_PORT)
+ sniffer = exit_stack.enter_context(
+ AsyncSnifferContext(
+ filter=f"tcp port {DEFAULT_TCP_SERVER_PORT}",
+ iface="lo",
+ session=session,
+ )
+ )
+
+ listen_socket = create_listen_socket(family=address_family)
+ listen_socket = exit_stack.enter_context(listen_socket)
+ exit_stack.enter_context(SimpleServerThread(listen_socket, mode="echo"))
+
+ client_socket = socket.socket(address_family, socket.SOCK_STREAM)
+ client_socket = exit_stack.push(client_socket)
+
+ set_tcp_authopt_key(
+ listen_socket,
+ tcp_authopt_key(alg=alg_id, key=master_key, include_options=include_options),
+ )
+ set_tcp_authopt_key(
+ client_socket,
+ tcp_authopt_key(alg=alg_id, key=master_key, include_options=include_options),
+ )
+
+ # even if one signature is incorrect keep processing the capture
+ old_nstat = nstat_json()
+ valkey = TcpAuthValidatorKey(
+ key=master_key, alg_name=alg_name, include_options=include_options
+ )
+ validator = TcpAuthValidator(keys=[valkey])
+
+ try:
+ client_socket.settimeout(1.0)
+ client_socket.connect(("localhost", DEFAULT_TCP_SERVER_PORT))
+ for _ in range(5):
+ check_socket_echo(client_socket)
+ except socket.timeout:
+ logger.warning("socket timeout", exc_info=True)
+ pass
+ client_socket.close()
+ session.wait_close()
+ sniffer.stop()
+
+ logger.info("capture: %r", sniffer.results)
+ for p in sniffer.results:
+ validator.handle_packet(p)
+
+ assert not validator.any_fail
+ assert not validator.any_unsigned
+ # Fails because of duplicate packets:
+ # assert not validator.any_incomplete
+ new_nstat = nstat_json()
+ assert (
+ 0
+ == new_nstat["kernel"]["TcpExtTCPAuthOptFailure"]
+ - old_nstat["kernel"]["TcpExtTCPAuthOptFailure"]
+ )
diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/validator.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/validator.py
new file mode 100644
index 000000000000..5f0e83421af5
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/validator.py
@@ -0,0 +1,158 @@
+# SPDX-License-Identifier: GPL-2.0
+from tcp_authopt_test.utils import scapy_tcp_get_authopt_val
+import typing
+import logging
+
+from dataclasses import dataclass
+from scapy.packet import Packet
+from scapy.layers.inet import TCP
+from . import tcp_authopt_alg
+from .tcp_authopt_alg import IPvXAddress, TCPAuthContext
+from .tcp_authopt_alg import get_scapy_ipvx_src
+from .tcp_authopt_alg import get_scapy_ipvx_dst
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class TCPSocketPair:
+ """TCP connection identifier"""
+
+ saddr: IPvXAddress = None
+ daddr: IPvXAddress = None
+ sport: int = 0
+ dport: int = 0
+
+ def rev(self) -> "TCPSocketPair":
+ return TCPSocketPair(self.daddr, self.saddr, self.dport, self.sport)
+
+
+@dataclass
+class TcpAuthValidatorKey:
+ key: bytes
+ alg_name: str
+ include_options: bool = True
+ keyid: typing.Optional[int] = None
+ sport: typing.Optional[int] = None
+ dport: typing.Optional[int] = None
+
+ def match_packet(self, p: Packet):
+ if not TCP in p:
+ return False
+ authopt = scapy_tcp_get_authopt_val(p[TCP])
+ if authopt is None:
+ return False
+ if self.keyid is not None and authopt.keyid != self.keyid:
+ return False
+ if self.sport is not None and p[TCP].sport != self.sport:
+ return False
+ if self.dport is not None and p[TCP].dport != self.dport:
+ return False
+ return True
+
+ def get_alg_imp(self):
+ return tcp_authopt_alg.get_alg(self.alg_name)
+
+
+def is_init_syn(p: Packet) -> bool:
+ return p[TCP].flags.S and not p[TCP].flags.A
+
+
+class TcpAuthValidator:
+ """Validate TCP auth sessions inside a capture"""
+
+ keys: typing.List[TcpAuthValidatorKey]
+ conn_dict: typing.Dict[TCPSocketPair, TCPAuthContext]
+ any_incomplete: bool = False
+ any_unsigned: bool = False
+ any_fail: bool = False
+
+ def __init__(self, keys=None):
+ self.keys = keys or []
+ self.conn_dict = {}
+
+ def get_key_for_packet(self, p):
+ for k in self.keys:
+ if k.match_packet(p):
+ return k
+ return None
+
+ def handle_packet(self, p: Packet):
+ if TCP not in p:
+ logger.debug("skip non-TCP packet")
+ return
+ key = self.get_key_for_packet(p)
+ if not key:
+ self.any_unsigned = True
+ logger.debug("skip packet not matching any known keys: %r", p)
+ return
+ authopt = scapy_tcp_get_authopt_val(p[TCP])
+ if not authopt:
+ self.any_unsigned = True
+ logger.debug("skip packet without tcp authopt: %r", p)
+ return
+ captured_mac = authopt.mac
+
+ saddr = get_scapy_ipvx_src(p)
+ daddr = get_scapy_ipvx_dst(p)
+
+ conn_key = TCPSocketPair(saddr, daddr, p[TCP].sport, p[TCP].dport)
+ if p[TCP].flags.S:
+ conn = self.conn_dict.get(conn_key, None)
+ if conn is not None:
+ logger.warning("overwrite %r", conn)
+ self.any_incomplete = True
+ conn = TCPAuthContext()
+ conn.saddr = saddr
+ conn.daddr = daddr
+ conn.sport = p[TCP].sport
+ conn.dport = p[TCP].dport
+ self.conn_dict[conn_key] = conn
+
+ if p[TCP].flags.A == False:
+ # SYN
+ conn.sisn = p[TCP].seq
+ conn.disn = 0
+ logger.info("Initialized for SYN: %r", conn)
+ else:
+ # SYN/ACK
+ conn.sisn = p[TCP].seq
+ conn.disn = p[TCP].ack - 1
+ logger.info("Initialized for SYNACK: %r", conn)
+
+ # Update opposite connection with dst_isn
+ rconn_key = conn_key.rev()
+ rconn = self.conn_dict.get(rconn_key, None)
+ if rconn is None:
+ logger.warning("missing SYN for SYNACK: %s", rconn_key)
+ self.any_incomplete = True
+ else:
+ assert rconn.sisn == conn.disn
+ assert rconn.disn == 0 or rconn.disn == conn.sisn
+ rconn.disn = conn.sisn
+ rconn.update_from_synack_packet(p)
+ logger.info("Updated peer for SYNACK: %r", rconn)
+ else:
+ conn = self.conn_dict.get(conn_key, None)
+ if conn is None:
+ logger.warning("missing TCP syn for %r", conn_key)
+ self.any_incomplete = True
+ return
+ # logger.debug("conn %r found for packet %r", conn, p)
+
+ context_bytes = conn.pack(syn=is_init_syn(p))
+ alg = key.get_alg_imp()
+ traffic_key = alg.kdf(key.key, context_bytes)
+ message_bytes = tcp_authopt_alg.build_message_from_scapy(
+ p, include_options=key.include_options
+ )
+ computed_mac = alg.mac(traffic_key, message_bytes)
+ if computed_mac == captured_mac:
+ logger.debug("ok - mac %s", computed_mac.hex())
+ else:
+ self.any_fail = True
+ logger.error(
+ "not ok - captured %s computed %s",
+ captured_mac.hex(),
+ computed_mac.hex(),
+ )
--
2.25.1

2021-08-24 21:36:33

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 05/15] tcp: authopt: Add crypto initialization

The crypto_shash API is used in order to compute packet signatures. The
API comes with several unfortunate limitations:

1) Allocating a crypto_shash can sleep and must be done in user context.
2) Packet signatures must be computed in softirq context
3) Packet signatures use dynamic "traffic keys" which require exclusive
access to crypto_shash for crypto_setkey.

The solution is to allocate one crypto_shash for each possible cpu for
each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
softirq context, signatures are computed and the tfm is returned.

The pool for each algorithm is reference counted, initialized at
setsockopt time and released in tcp_authopt_key_info's rcu callback

Signed-off-by: Leonard Crestez <[email protected]>
---
include/net/tcp_authopt.h | 3 +
net/ipv4/tcp_authopt.c | 177 +++++++++++++++++++++++++++++++++++++-
2 files changed, 178 insertions(+), 2 deletions(-)

diff --git a/include/net/tcp_authopt.h b/include/net/tcp_authopt.h
index b4277112b506..c9ee2059b442 100644
--- a/include/net/tcp_authopt.h
+++ b/include/net/tcp_authopt.h
@@ -2,10 +2,12 @@
#ifndef _LINUX_TCP_AUTHOPT_H
#define _LINUX_TCP_AUTHOPT_H

#include <uapi/linux/tcp.h>

+struct tcp_authopt_alg_imp;
+
/**
* struct tcp_authopt_key_info - Representation of a Master Key Tuple as per RFC5925
*
* Key structure lifetime is only protected by RCU so readers needs to hold a
* single rcu_read_lock until they're done with the key.
@@ -20,10 +22,11 @@ struct tcp_authopt_key_info {
u8 send_id, recv_id;
u8 alg_id;
u8 keylen;
u8 key[TCP_AUTHOPT_MAXKEYLEN];
struct sockaddr_storage addr;
+ struct tcp_authopt_alg_imp *alg;
};

/**
* struct tcp_authopt_info - Per-socket information regarding tcp_authopt
*
diff --git a/net/ipv4/tcp_authopt.c b/net/ipv4/tcp_authopt.c
index f6dddc5775ff..ce560bd88903 100644
--- a/net/ipv4/tcp_authopt.c
+++ b/net/ipv4/tcp_authopt.c
@@ -4,10 +4,161 @@
#include <net/tcp.h>
#include <net/tcp_authopt.h>
#include <crypto/hash.h>
#include <trace/events/tcp.h>

+/* All current algorithms have a mac length of 12 but crypto API digestsize can be larger */
+#define TCP_AUTHOPT_MAXMACBUF 20
+#define TCP_AUTHOPT_MAX_TRAFFIC_KEY_LEN 20
+
+struct tcp_authopt_alg_imp {
+ /* Name of algorithm in crypto-api */
+ const char *alg_name;
+ /* One of the TCP_AUTHOPT_ALG_* constants from uapi */
+ u8 alg_id;
+ /* Length of traffic key */
+ u8 traffic_key_len;
+ /* Length of mac in TCP option */
+ u8 maclen;
+
+ /* shared crypto_shash */
+ spinlock_t lock;
+ int ref_cnt;
+ struct crypto_shash *tfm;
+};
+
+static struct tcp_authopt_alg_imp tcp_authopt_alg_list[] = {
+ {
+ .alg_id = TCP_AUTHOPT_ALG_HMAC_SHA_1_96,
+ .alg_name = "hmac(sha1)",
+ .traffic_key_len = 20,
+ .maclen = 12,
+ .lock = __SPIN_LOCK_UNLOCKED(tcp_authopt_alg_list[0].lock),
+ },
+ {
+ .alg_id = TCP_AUTHOPT_ALG_AES_128_CMAC_96,
+ .alg_name = "cmac(aes)",
+ .traffic_key_len = 16,
+ .maclen = 12,
+ .lock = __SPIN_LOCK_UNLOCKED(tcp_authopt_alg_list[1].lock),
+ },
+};
+
+/* get a pointer to the tcp_authopt_alg instance or NULL if id invalid */
+static inline struct tcp_authopt_alg_imp *tcp_authopt_alg_get(int alg_num)
+{
+ if (alg_num <= 0 || alg_num > 2)
+ return NULL;
+ return &tcp_authopt_alg_list[alg_num - 1];
+}
+
+/* Mark an algorithm as in-use from user context */
+static int tcp_authopt_alg_require(struct tcp_authopt_alg_imp *alg)
+{
+ struct crypto_shash *tfm = NULL;
+ bool need_init = false;
+
+ might_sleep();
+
+ /* If we're the first user then we need to initialize shash but we might lose the race. */
+ spin_lock_bh(&alg->lock);
+ WARN_ON(alg->ref_cnt < 0);
+ if (alg->ref_cnt == 0)
+ need_init = true;
+ else
+ ++alg->ref_cnt;
+ spin_unlock_bh(&alg->lock);
+
+ /* Already initialized */
+ if (!need_init)
+ return 0;
+
+ tfm = crypto_alloc_shash(alg->alg_name, 0, 0);
+ if (IS_ERR(tfm))
+ return PTR_ERR(tfm);
+
+ spin_lock_bh(&alg->lock);
+ if (alg->ref_cnt == 0)
+ /* race won */
+ alg->tfm = tfm;
+ else
+ /* race lost, free tfm later */
+ need_init = false;
+ ++alg->ref_cnt;
+ spin_unlock_bh(&alg->lock);
+
+ if (!need_init)
+ crypto_free_shash(tfm);
+ else
+ pr_info("initialized tcp-ao %s", alg->alg_name);
+
+ return 0;
+}
+
+static void tcp_authopt_alg_release(struct tcp_authopt_alg_imp *alg)
+{
+ struct crypto_shash *tfm_to_free = NULL;
+
+ spin_lock_bh(&alg->lock);
+ --alg->ref_cnt;
+ WARN_ON(alg->ref_cnt < 0);
+ if (alg->ref_cnt == 0) {
+ tfm_to_free = alg->tfm;
+ alg->tfm = NULL;
+ }
+ spin_unlock_bh(&alg->lock);
+
+ if (tfm_to_free) {
+ pr_info("released tcp-ao %s", alg->alg_name);
+ crypto_free_shash(tfm_to_free);
+ }
+}
+
+/* increase reference count on an algorithm that is already in use */
+static void tcp_authopt_alg_incref(struct tcp_authopt_alg_imp *alg)
+{
+ spin_lock_bh(&alg->lock);
+ WARN_ON(alg->ref_cnt <= 0);
+ ++alg->ref_cnt;
+ spin_unlock_bh(&alg->lock);
+}
+
+static struct crypto_shash *tcp_authopt_alg_get_tfm(struct tcp_authopt_alg_imp *alg)
+{
+ spin_lock_bh(&alg->lock);
+ WARN_ON(alg->ref_cnt < 0);
+ return alg->tfm;
+}
+
+static void tcp_authopt_alg_put_tfm(struct tcp_authopt_alg_imp *alg, struct crypto_shash *tfm)
+{
+ WARN_ON(tfm != alg->tfm);
+ spin_unlock_bh(&alg->lock);
+}
+
+static struct crypto_shash *tcp_authopt_get_kdf_shash(struct tcp_authopt_key_info *key)
+{
+ return tcp_authopt_alg_get_tfm(key->alg);
+}
+
+static void tcp_authopt_put_kdf_shash(struct tcp_authopt_key_info *key,
+ struct crypto_shash *tfm)
+{
+ return tcp_authopt_alg_put_tfm(key->alg, tfm);
+}
+
+static struct crypto_shash *tcp_authopt_get_mac_shash(struct tcp_authopt_key_info *key)
+{
+ return tcp_authopt_alg_get_tfm(key->alg);
+}
+
+static void tcp_authopt_put_mac_shash(struct tcp_authopt_key_info *key,
+ struct crypto_shash *tfm)
+{
+ return tcp_authopt_alg_put_tfm(key->alg, tfm);
+}
+
/* checks that ipv4 or ipv6 addr matches. */
static bool ipvx_addr_match(struct sockaddr_storage *a1,
struct sockaddr_storage *a2)
{
if (a1->ss_family != a2->ss_family)
@@ -118,17 +269,25 @@ int tcp_get_authopt_val(struct sock *sk, struct tcp_authopt *opt)
opt->flags = info->flags & TCP_AUTHOPT_KNOWN_FLAGS;

return 0;
}

+static void tcp_authopt_key_free_rcu(struct rcu_head *rcu)
+{
+ struct tcp_authopt_key_info *key = container_of(rcu, struct tcp_authopt_key_info, rcu);
+
+ tcp_authopt_alg_release(key->alg);
+ kfree(key);
+}
+
static void tcp_authopt_key_del(struct sock *sk,
struct tcp_authopt_info *info,
struct tcp_authopt_key_info *key)
{
hlist_del_rcu(&key->node);
atomic_sub(sizeof(*key), &sk->sk_omem_alloc);
- kfree_rcu(key, rcu);
+ call_rcu(&key->rcu, tcp_authopt_key_free_rcu);
}

/* free info and keys but don't touch tp->authopt_info */
static void __tcp_authopt_info_free(struct sock *sk, struct tcp_authopt_info *info)
{
@@ -160,10 +319,12 @@ void tcp_authopt_clear(struct sock *sk)
int tcp_set_authopt_key(struct sock *sk, sockptr_t optval, unsigned int optlen)
{
struct tcp_authopt_key opt;
struct tcp_authopt_info *info;
struct tcp_authopt_key_info *key_info;
+ struct tcp_authopt_alg_imp *alg;
+ int err;

sock_owned_by_me(sk);

/* If userspace optlen is too short fill the rest with zeros */
if (optlen > sizeof(opt))
@@ -199,23 +360,35 @@ int tcp_set_authopt_key(struct sock *sk, sockptr_t optval, unsigned int optlen)
/* Initialize tcp_authopt_info if not already set */
info = __tcp_authopt_info_get_or_create(sk);
if (IS_ERR(info))
return PTR_ERR(info);

+ /* check the algorithm */
+ alg = tcp_authopt_alg_get(opt.alg);
+ if (!alg)
+ return -EINVAL;
+ WARN_ON(alg->alg_id != opt.alg);
+ err = tcp_authopt_alg_require(alg);
+ if (err)
+ return err;
+
/* If an old key exists with exact ID then remove and replace.
* RCU-protected readers might observe both and pick any.
*/
key_info = tcp_authopt_key_lookup_exact(sk, info, &opt);
if (key_info)
tcp_authopt_key_del(sk, info, key_info);
key_info = sock_kmalloc(sk, sizeof(*key_info), GFP_KERNEL | __GFP_ZERO);
- if (!key_info)
+ if (!key_info) {
+ tcp_authopt_alg_release(alg);
return -ENOMEM;
+ }
key_info->flags = opt.flags & TCP_AUTHOPT_KEY_KNOWN_FLAGS;
key_info->send_id = opt.send_id;
key_info->recv_id = opt.recv_id;
key_info->alg_id = opt.alg;
+ key_info->alg = alg;
key_info->keylen = opt.keylen;
memcpy(key_info->key, opt.key, opt.keylen);
memcpy(&key_info->addr, &opt.addr, sizeof(key_info->addr));
hlist_add_head_rcu(&key_info->node, &info->head);

--
2.25.1

2021-08-24 21:36:37

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 12/15] selftests: Initial tcp_authopt support for fcnal-test

Just test that a correct password is passed or otherwise a timeout is
obtained.

Signed-off-by: Leonard Crestez <[email protected]>
---
tools/testing/selftests/net/fcnal-test.sh | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)

diff --git a/tools/testing/selftests/net/fcnal-test.sh b/tools/testing/selftests/net/fcnal-test.sh
index 162e5f1ac36b..ca3b90f6fecb 100755
--- a/tools/testing/selftests/net/fcnal-test.sh
+++ b/tools/testing/selftests/net/fcnal-test.sh
@@ -788,10 +788,31 @@ ipv4_ping()
}

################################################################################
# IPv4 TCP

+#
+# TCP Authentication Option Tests
+#
+ipv4_tcp_authopt()
+{
+ # basic use case
+ log_start
+ run_cmd nettest -s -A ${MD5_PW} &
+ sleep 1
+ run_cmd_nsb nettest -r ${NSA_IP} -A ${MD5_PW}
+ log_test $? 0 "AO: Simple password"
+
+ # wrong password
+ log_start
+ show_hint "Should timeout since client uses wrong password"
+ run_cmd nettest -s -A ${MD5_PW} &
+ sleep 1
+ run_cmd_nsb nettest -r ${NSA_IP} -A ${MD5_WRONG_PW}
+ log_test $? 2 "AO: Client uses wrong password"
+}
+
#
# MD5 tests without VRF
#
ipv4_tcp_md5_novrf()
{
@@ -1119,10 +1140,11 @@ ipv4_tcp_novrf()
show_hint "Should fail 'Connection refused'"
run_cmd nettest -d ${NSA_DEV} -r ${a}
log_test_addr ${a} $? 1 "No server, device client, local conn"

ipv4_tcp_md5_novrf
+ ipv4_tcp_authopt
}

ipv4_tcp_vrf()
{
local a
--
2.25.1

2021-08-24 21:36:37

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 03/15] selftests: Initial tcp_authopt test module

This test suite is written as a standalone python3 package using
dependencies such as scapy.

The run.sh script wrapper called from kselftest infrastructure uses
"tox" to generate an isolated virtual environment just for running these
tests. The run.sh wrapper can be called from anywhere and does not rely
on kselftest infrastructure.

The python3 and tox packages be installed manually but not any other
dependencies

Signed-off-by: Leonard Crestez <[email protected]>
---
tools/testing/selftests/tcp_authopt/Makefile | 5 +++++
tools/testing/selftests/tcp_authopt/README.rst | 15 +++++++++++++++
tools/testing/selftests/tcp_authopt/config | 6 ++++++
tools/testing/selftests/tcp_authopt/run.sh | 11 +++++++++++
tools/testing/selftests/tcp_authopt/setup.cfg | 17 +++++++++++++++++
tools/testing/selftests/tcp_authopt/setup.py | 5 +++++
.../tcp_authopt/tcp_authopt_test/__init__.py | 0
7 files changed, 59 insertions(+)
create mode 100644 tools/testing/selftests/tcp_authopt/Makefile
create mode 100644 tools/testing/selftests/tcp_authopt/README.rst
create mode 100644 tools/testing/selftests/tcp_authopt/config
create mode 100755 tools/testing/selftests/tcp_authopt/run.sh
create mode 100644 tools/testing/selftests/tcp_authopt/setup.cfg
create mode 100644 tools/testing/selftests/tcp_authopt/setup.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/__init__.py

diff --git a/tools/testing/selftests/tcp_authopt/Makefile b/tools/testing/selftests/tcp_authopt/Makefile
new file mode 100644
index 000000000000..391412071875
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+include ../lib.mk
+
+TEST_PROGS += ./run.sh
+TEST_FILES := setup.py setup.cfg tcp_authopt_test
diff --git a/tools/testing/selftests/tcp_authopt/README.rst b/tools/testing/selftests/tcp_authopt/README.rst
new file mode 100644
index 000000000000..e9e4acc0a22a
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/README.rst
@@ -0,0 +1,15 @@
+=========================================
+Tests for linux TCP Authentication Option
+=========================================
+
+Test suite is written in python3 using pytest and scapy. The test suite is
+mostly self-contained as a python package.
+
+The recommended way to run this is the included `run.sh` script as root, this
+will automatically create a virtual environment with the correct dependencies
+using `tox`.
+
+An old separate version can be found here: https://github.com/cdleonard/tcp-authopt-test
+
+Integration with kselftest infrastructure is minimal: when in doubt just run
+this separately.
diff --git a/tools/testing/selftests/tcp_authopt/config b/tools/testing/selftests/tcp_authopt/config
new file mode 100644
index 000000000000..0d4e5d47fa72
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/config
@@ -0,0 +1,6 @@
+# RFC5925 TCP Authentication Option and all algorithms
+CONFIG_TCP_AUTHOPT=y
+CONFIG_CRYPTO_SHA1=M
+CONFIG_CRYPTO_HMAC=M
+CONFIG_CRYPTO_AES=M
+CONFIG_CRYPTO_CMAC=M
diff --git a/tools/testing/selftests/tcp_authopt/run.sh b/tools/testing/selftests/tcp_authopt/run.sh
new file mode 100755
index 000000000000..192ae094e3be
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/run.sh
@@ -0,0 +1,11 @@
+#! /bin/bash
+
+if ! command -v tox >/dev/null; then
+ echo >&2 "error: please install the python tox package"
+ exit 1
+fi
+if [[ $(id -u) -ne 0 ]]; then
+ echo >&2 "warning: running as non-root user is unlikely to work"
+fi
+cd "$(dirname "${BASH_SOURCE[0]}")"
+exec tox "$@"
diff --git a/tools/testing/selftests/tcp_authopt/setup.cfg b/tools/testing/selftests/tcp_authopt/setup.cfg
new file mode 100644
index 000000000000..373c5632b0a2
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/setup.cfg
@@ -0,0 +1,17 @@
+[options]
+install_requires=
+ cryptography
+ nsenter
+ pre-commit
+ pytest
+ scapy
+
+[tox:tox]
+envlist = py3
+
+[testenv]
+commands = pytest {posargs}
+
+[metadata]
+name = tcp-authopt-test
+version = 0.1
diff --git a/tools/testing/selftests/tcp_authopt/setup.py b/tools/testing/selftests/tcp_authopt/setup.py
new file mode 100644
index 000000000000..d5e50aa1ca5e
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/setup.py
@@ -0,0 +1,5 @@
+#! /usr/bin/env python3
+
+from setuptools import setup
+
+setup()
diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/__init__.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
--
2.25.1

2021-08-24 21:36:38

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 11/15] selftests: Initial tcp_authopt support for nettest

Add support for configuring TCP Authentication Option. Only a single key
with default options.

Signed-off-by: Leonard Crestez <[email protected]>
---
tools/testing/selftests/net/nettest.c | 34 ++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/net/nettest.c b/tools/testing/selftests/net/nettest.c
index bd6288302094..f04c6af79129 100644
--- a/tools/testing/selftests/net/nettest.c
+++ b/tools/testing/selftests/net/nettest.c
@@ -100,10 +100,12 @@ struct sock_args {
struct sockaddr_in v4;
struct sockaddr_in6 v6;
} md5_prefix;
unsigned int prefix_len;

+ const char *authopt_password;
+
/* expected addresses and device index for connection */
const char *expected_dev;
const char *expected_server_dev;
int expected_ifindex;

@@ -250,10 +252,27 @@ static int switch_ns(const char *ns)
close(fd);

return ret;
}

+static int tcp_set_authopt(int sd, struct sock_args *args)
+{
+ struct tcp_authopt_key key;
+ int rc;
+
+ memset(&key, 0, sizeof(key));
+ strcpy((char *)key.key, args->authopt_password);
+ key.keylen = strlen(args->authopt_password);
+ key.alg = TCP_AUTHOPT_ALG_HMAC_SHA_1_96;
+
+ rc = setsockopt(sd, IPPROTO_TCP, TCP_AUTHOPT_KEY, &key, sizeof(key));
+ if (rc < 0)
+ log_err_errno("setsockopt(TCP_AUTHOPT_KEY)");
+
+ return rc;
+}
+
static int tcp_md5sig(int sd, void *addr, socklen_t alen, struct sock_args *args)
{
int keylen = strlen(args->password);
struct tcp_md5sig md5sig = {};
int opt = TCP_MD5SIG;
@@ -1508,10 +1527,15 @@ static int do_server(struct sock_args *args, int ipc_fd)
if (args->password && tcp_md5_remote(lsd, args)) {
close(lsd);
goto err_exit;
}

+ if (args->authopt_password && tcp_set_authopt(lsd, args)) {
+ close(lsd);
+ goto err_exit;
+ }
+
ipc_write(ipc_fd, 1);
while (1) {
log_msg("waiting for client connection.\n");
FD_ZERO(&rfds);
FD_SET(lsd, &rfds);
@@ -1630,10 +1654,13 @@ static int connectsock(void *addr, socklen_t alen, struct sock_args *args)
goto out;

if (args->password && tcp_md5sig(sd, addr, alen, args))
goto err;

+ if (args->authopt_password && tcp_set_authopt(sd, args))
+ goto err;
+
if (args->bind_test_only)
goto out;

if (connect(sd, addr, alen) < 0) {
if (errno != EINPROGRESS) {
@@ -1819,11 +1846,11 @@ static int ipc_parent(int cpid, int fd, struct sock_args *args)

wait(&status);
return client_status;
}

-#define GETOPT_STR "sr:l:c:p:t:g:P:DRn:M:X:m:d:I:BN:O:SCi6xL:0:1:2:3:Fbq"
+#define GETOPT_STR "sr:l:c:p:t:g:P:DRn:M:X:m:A:d:I:BN:O:SCi6xL:0:1:2:3:Fbq"

static void print_usage(char *prog)
{
printf(
"usage: %s OPTS\n"
@@ -1856,10 +1883,12 @@ static void print_usage(char *prog)
" -n num number of times to send message\n"
"\n"
" -M password use MD5 sum protection\n"
" -X password MD5 password for client mode\n"
" -m prefix/len prefix and length to use for MD5 key\n"
+ " -A password use RFC5925 TCP Authentication option\n"
+ "\n"
" -g grp multicast group (e.g., 239.1.1.1)\n"
" -i interactive mode (default is echo and terminate)\n"
"\n"
" -0 addr Expected local address\n"
" -1 addr Expected remote address\n"
@@ -1970,10 +1999,13 @@ int main(int argc, char *argv[])
args.client_pw = optarg;
break;
case 'm':
args.md5_prefix_str = optarg;
break;
+ case 'A':
+ args.authopt_password = optarg;
+ break;
case 'S':
args.use_setsockopt = 1;
break;
case 'C':
args.use_cmsg = 1;
--
2.25.1

2021-08-24 21:36:40

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 09/15] selftests: tcp_authopt: Test key address binding

By default TCP-AO keys apply to all possible peers but it's possible to
have different keys for different remote hosts.

This patch adds initial tests for the behavior behind the
TCP_AUTHOPT_KEY_BIND_ADDR flag. Server rejection is tested via client
timeout so this can be slightly slow.

Signed-off-by: Leonard Crestez <[email protected]>
---
.../tcp_authopt_test/netns_fixture.py | 63 +++++++
.../tcp_authopt/tcp_authopt_test/server.py | 82 ++++++++++
.../tcp_authopt/tcp_authopt_test/test_bind.py | 143 ++++++++++++++++
.../tcp_authopt/tcp_authopt_test/utils.py | 154 ++++++++++++++++++
4 files changed, 442 insertions(+)
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/netns_fixture.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/server.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_bind.py
create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/utils.py

diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/netns_fixture.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/netns_fixture.py
new file mode 100644
index 000000000000..20bb12c2aae2
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/netns_fixture.py
@@ -0,0 +1,63 @@
+# SPDX-License-Identifier: GPL-2.0
+import subprocess
+import socket
+from ipaddress import IPv4Address
+from ipaddress import IPv6Address
+
+
+class NamespaceFixture:
+ """Create a pair of namespaces connected by one veth pair
+
+ Each end of the pair has multiple addresses but everything is in the same subnet
+ """
+
+ ns1_name = "tcp_authopt_test_1"
+ ns2_name = "tcp_authopt_test_2"
+
+ @classmethod
+ def get_ipv4_addr(cls, ns=1, index=1) -> IPv4Address:
+ return IPv4Address("10.10.0.0") + (ns << 8) + index
+
+ @classmethod
+ def get_ipv6_addr(cls, ns=1, index=1) -> IPv6Address:
+ return IPv6Address("fd00::") + (ns << 16) + index
+
+ @classmethod
+ def get_addr(cls, address_family=socket.AF_INET, ns=1, index=1):
+ if address_family == socket.AF_INET:
+ return cls.get_ipv4_addr(ns, index)
+ elif address_family == socket.AF_INET6:
+ return cls.get_ipv6_addr(ns, index)
+ else:
+ raise ValueError(f"Bad address_family={address_family}")
+
+ def __init__(self, **kw):
+ for k, v in kw.items():
+ setattr(self, k, v)
+
+ def __enter__(self):
+ script = f"""
+set -e -x
+ip netns del {self.ns1_name} || true
+ip netns del {self.ns2_name} || true
+ip netns add {self.ns1_name}
+ip netns add {self.ns2_name}
+ip link add veth0 netns {self.ns1_name} type veth peer name veth0 netns {self.ns2_name}
+ip netns exec {self.ns1_name} ip link set veth0 up
+ip netns exec {self.ns2_name} ip link set veth0 up
+"""
+ for index in [1, 2, 3]:
+ script += f"ip -n {self.ns1_name} addr add {self.get_ipv4_addr(1, index)}/16 dev veth0\n"
+ script += f"ip -n {self.ns2_name} addr add {self.get_ipv4_addr(2, index)}/16 dev veth0\n"
+ script += f"ip -n {self.ns1_name} addr add {self.get_ipv6_addr(1, index)}/64 dev veth0 nodad\n"
+ script += f"ip -n {self.ns2_name} addr add {self.get_ipv6_addr(2, index)}/64 dev veth0 nodad\n"
+ subprocess.run(script, shell=True, check=True)
+ return self
+
+ def __exit__(self, *a):
+ script = f"""
+set -e -x
+ip netns del {self.ns1_name} || true
+ip netns del {self.ns2_name} || true
+"""
+ subprocess.run(script, shell=True, check=True)
diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/server.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/server.py
new file mode 100644
index 000000000000..c4cce8f5862a
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/server.py
@@ -0,0 +1,82 @@
+# SPDX-License-Identifier: GPL-2.0
+import logging
+import os
+import selectors
+from contextlib import ExitStack
+from threading import Thread
+
+logger = logging.getLogger(__name__)
+
+
+class SimpleServerThread(Thread):
+ """Simple server thread for testing TCP sockets
+
+ All data is read in 1000 bytes chunks and either echoed back or discarded.
+ """
+
+ def __init__(self, socket, mode="recv"):
+ self.listen_socket = socket
+ self.server_socket = []
+ self.mode = mode
+ super().__init__()
+
+ def _read(self, conn, events):
+ # logger.debug("events=%r", events)
+ data = conn.recv(1000)
+ # logger.debug("len(data)=%r", len(data))
+ if len(data) == 0:
+ # logger.info("closing %r", conn)
+ conn.close()
+ self.sel.unregister(conn)
+ else:
+ if self.mode == "echo":
+ conn.sendall(data)
+ elif self.mode == "recv":
+ pass
+ else:
+ raise ValueError(f"Unknown mode {self.mode}")
+
+ def _stop_pipe_read(self, conn, events):
+ self.should_loop = False
+
+ def start(self) -> None:
+ self.exit_stack = ExitStack()
+ self._stop_pipe_rfd, self._stop_pipe_wfd = os.pipe()
+ self.exit_stack.callback(lambda: os.close(self._stop_pipe_rfd))
+ self.exit_stack.callback(lambda: os.close(self._stop_pipe_wfd))
+ return super().start()
+
+ def _accept(self, conn, events):
+ assert conn == self.listen_socket
+ conn, _addr = self.listen_socket.accept()
+ conn = self.exit_stack.enter_context(conn)
+ conn.setblocking(False)
+ self.sel.register(conn, selectors.EVENT_READ, self._read)
+ self.server_socket.append(conn)
+
+ def run(self):
+ self.should_loop = True
+ self.sel = self.exit_stack.enter_context(selectors.DefaultSelector())
+ self.sel.register(
+ self._stop_pipe_rfd, selectors.EVENT_READ, self._stop_pipe_read
+ )
+ self.sel.register(self.listen_socket, selectors.EVENT_READ, self._accept)
+ # logger.debug("loop init")
+ while self.should_loop:
+ for key, events in self.sel.select(timeout=1):
+ callback = key.data
+ callback(key.fileobj, events)
+ # logger.debug("loop done")
+
+ def stop(self):
+ """Try to stop nicely"""
+ os.write(self._stop_pipe_wfd, b"Q")
+ self.join()
+ self.exit_stack.close()
+
+ def __enter__(self):
+ self.start()
+ return self
+
+ def __exit__(self, *args):
+ self.stop()
diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_bind.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_bind.py
new file mode 100644
index 000000000000..980954098e97
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_bind.py
@@ -0,0 +1,143 @@
+# SPDX-License-Identifier: GPL-2.0
+"""Test TCP-AO keys can be bound to specific remote addresses"""
+from contextlib import ExitStack
+import socket
+import pytest
+from .netns_fixture import NamespaceFixture
+from .utils import create_listen_socket
+from .server import SimpleServerThread
+from . import linux_tcp_authopt
+from .linux_tcp_authopt import (
+ tcp_authopt,
+ set_tcp_authopt,
+ set_tcp_authopt_key,
+ tcp_authopt_key,
+)
+from .utils import netns_context, DEFAULT_TCP_SERVER_PORT, check_socket_echo
+from .conftest import skipif_missing_tcp_authopt
+
+pytestmark = skipif_missing_tcp_authopt
+
+
[email protected]("address_family", [socket.AF_INET, socket.AF_INET6])
+def test_addr_server_bind(exit_stack: ExitStack, address_family):
+ """ "Server only accept client2, check client1 fails"""
+ nsfixture = exit_stack.enter_context(NamespaceFixture())
+ server_addr = str(nsfixture.get_addr(address_family, 1, 1))
+ client_addr = str(nsfixture.get_addr(address_family, 2, 1))
+ client_addr2 = str(nsfixture.get_addr(address_family, 2, 2))
+
+ # create server:
+ listen_socket = exit_stack.push(
+ create_listen_socket(family=address_family, ns=nsfixture.ns1_name)
+ )
+ exit_stack.enter_context(SimpleServerThread(listen_socket, mode="echo"))
+
+ # set keys:
+ server_key = tcp_authopt_key(
+ alg=linux_tcp_authopt.TCP_AUTHOPT_ALG_HMAC_SHA_1_96,
+ key="hello",
+ flags=linux_tcp_authopt.TCP_AUTHOPT_KEY_BIND_ADDR,
+ addr=client_addr2,
+ )
+ set_tcp_authopt(
+ listen_socket,
+ tcp_authopt(flags=linux_tcp_authopt.TCP_AUTHOPT_FLAG_REJECT_UNEXPECTED),
+ )
+ set_tcp_authopt_key(listen_socket, server_key)
+
+ # create client socket:
+ def create_client_socket():
+ with netns_context(nsfixture.ns2_name):
+ client_socket = socket.socket(address_family, socket.SOCK_STREAM)
+ client_key = tcp_authopt_key(
+ alg=linux_tcp_authopt.TCP_AUTHOPT_ALG_HMAC_SHA_1_96,
+ key="hello",
+ )
+ set_tcp_authopt_key(client_socket, client_key)
+ return client_socket
+
+ # addr match:
+ # with create_client_socket() as client_socket2:
+ # client_socket2.bind((client_addr2, 0))
+ # client_socket2.settimeout(1.0)
+ # client_socket2.connect((server_addr, TCP_SERVER_PORT))
+
+ # addr mismatch:
+ with create_client_socket() as client_socket1:
+ client_socket1.bind((client_addr, 0))
+ with pytest.raises(socket.timeout):
+ client_socket1.settimeout(1.0)
+ client_socket1.connect((server_addr, DEFAULT_TCP_SERVER_PORT))
+
+
[email protected]("address_family", [socket.AF_INET, socket.AF_INET6])
+def test_addr_client_bind(exit_stack: ExitStack, address_family):
+ """ "Client configures different keys with same id but different addresses"""
+ nsfixture = exit_stack.enter_context(NamespaceFixture())
+ server_addr1 = str(nsfixture.get_addr(address_family, 1, 1))
+ server_addr2 = str(nsfixture.get_addr(address_family, 1, 2))
+ client_addr = str(nsfixture.get_addr(address_family, 2, 1))
+
+ # create servers:
+ listen_socket1 = exit_stack.enter_context(
+ create_listen_socket(
+ family=address_family, ns=nsfixture.ns1_name, bind_addr=server_addr1
+ )
+ )
+ listen_socket2 = exit_stack.enter_context(
+ create_listen_socket(
+ family=address_family, ns=nsfixture.ns1_name, bind_addr=server_addr2
+ )
+ )
+ exit_stack.enter_context(SimpleServerThread(listen_socket1, mode="echo"))
+ exit_stack.enter_context(SimpleServerThread(listen_socket2, mode="echo"))
+
+ # set keys:
+ set_tcp_authopt_key(
+ listen_socket1,
+ tcp_authopt_key(
+ alg=linux_tcp_authopt.TCP_AUTHOPT_ALG_HMAC_SHA_1_96,
+ key="11111",
+ ),
+ )
+ set_tcp_authopt_key(
+ listen_socket2,
+ tcp_authopt_key(
+ alg=linux_tcp_authopt.TCP_AUTHOPT_ALG_HMAC_SHA_1_96,
+ key="22222",
+ ),
+ )
+
+ # create client socket:
+ def create_client_socket():
+ with netns_context(nsfixture.ns2_name):
+ client_socket = socket.socket(address_family, socket.SOCK_STREAM)
+ set_tcp_authopt_key(
+ client_socket,
+ tcp_authopt_key(
+ alg=linux_tcp_authopt.TCP_AUTHOPT_ALG_HMAC_SHA_1_96,
+ key="11111",
+ flags=linux_tcp_authopt.TCP_AUTHOPT_KEY_BIND_ADDR,
+ addr=server_addr1,
+ ),
+ )
+ set_tcp_authopt_key(
+ client_socket,
+ tcp_authopt_key(
+ alg=linux_tcp_authopt.TCP_AUTHOPT_ALG_HMAC_SHA_1_96,
+ key="22222",
+ flags=linux_tcp_authopt.TCP_AUTHOPT_KEY_BIND_ADDR,
+ addr=server_addr2,
+ ),
+ )
+ client_socket.settimeout(1.0)
+ client_socket.bind((client_addr, 0))
+ return client_socket
+
+ with create_client_socket() as client_socket1:
+ client_socket1.connect((server_addr1, DEFAULT_TCP_SERVER_PORT))
+ check_socket_echo(client_socket1)
+ with create_client_socket() as client_socket2:
+ client_socket2.connect((server_addr2, DEFAULT_TCP_SERVER_PORT))
+ check_socket_echo(client_socket2)
diff --git a/tools/testing/selftests/tcp_authopt/tcp_authopt_test/utils.py b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/utils.py
new file mode 100644
index 000000000000..22bd3f0a142a
--- /dev/null
+++ b/tools/testing/selftests/tcp_authopt/tcp_authopt_test/utils.py
@@ -0,0 +1,154 @@
+# SPDX-License-Identifier: GPL-2.0
+import json
+import random
+import subprocess
+import threading
+import typing
+import socket
+from dataclasses import dataclass
+from contextlib import nullcontext
+
+from nsenter import Namespace
+from scapy.sendrecv import AsyncSniffer
+
+
+# TCP port does not impact Authentication Option so define a single default
+DEFAULT_TCP_SERVER_PORT = 17971
+
+
+class SimpleWaitEvent(threading.Event):
+ @property
+ def value(self) -> bool:
+ return self.is_set()
+
+ @value.setter
+ def value(self, value: bool):
+ if value:
+ self.set()
+ else:
+ self.clear()
+
+ def wait(self, timeout=None):
+ """Like Event.wait except raise on timeout"""
+ super().wait(timeout)
+ if not self.is_set():
+ raise TimeoutError(f"Timed out timeout={timeout!r}")
+
+
+def recvall(sock, todo):
+ """Receive exactly todo bytes unless EOF"""
+ data = bytes()
+ while True:
+ chunk = sock.recv(todo)
+ if not len(chunk):
+ return data
+ data += chunk
+ todo -= len(chunk)
+ if todo == 0:
+ return data
+ assert todo > 0
+
+
+def randbytes(count) -> bytes:
+ """Return a random byte array"""
+ return bytes([random.randint(0, 255) for index in range(count)])
+
+
+def check_socket_echo(sock, size=1024):
+ """Send random bytes and check they are received"""
+ send_buf = randbytes(size)
+ sock.sendall(send_buf)
+ recv_buf = recvall(sock, size)
+ assert send_buf == recv_buf
+
+
+def nstat_json(command_prefix: str = ""):
+ """Parse nstat output into a python dict"""
+ runres = subprocess.run(
+ f"{command_prefix}nstat -a --zeros --json",
+ shell=True,
+ check=True,
+ stdout=subprocess.PIPE,
+ encoding="utf-8",
+ )
+ return json.loads(runres.stdout)
+
+
+def netns_context(ns: str = ""):
+ """Create context manager for a certain optional netns
+
+ If the ns argument is empty then just return a `nullcontext`
+ """
+ if ns:
+ return Namespace("/var/run/netns/" + ns, "net")
+ else:
+ return nullcontext()
+
+
+def create_listen_socket(
+ ns: str = "",
+ family=socket.AF_INET,
+ reuseaddr=True,
+ listen_depth=10,
+ bind_addr="",
+ bind_port=DEFAULT_TCP_SERVER_PORT,
+):
+ with netns_context(ns):
+ listen_socket = socket.socket(family, socket.SOCK_STREAM)
+ if reuseaddr:
+ listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ listen_socket.bind((str(bind_addr), bind_port))
+ listen_socket.listen(listen_depth)
+ return listen_socket
+
+
+@dataclass
+class tcphdr_authopt:
+ """Representation of a TCP auth option as it appears in a TCP packet"""
+
+ keyid: int
+ rnextkeyid: int
+ mac: bytes
+
+ @classmethod
+ def unpack(cls, buf) -> "tcphdr_authopt":
+ return cls(buf[0], buf[1], buf[2:])
+
+ def __repr__(self):
+ return f"tcphdr_authopt({self.keyid}, {self.rnextkeyid}, bytes.fromhex({self.mac.hex(' ')!r})"
+
+
+def scapy_tcp_get_authopt_val(tcp) -> typing.Optional[tcphdr_authopt]:
+ for optnum, optval in tcp.options:
+ if optnum == 29:
+ return tcphdr_authopt.unpack(optval)
+ return None
+
+
+def scapy_sniffer_start_block(sniffer: AsyncSniffer, timeout=1):
+ """Like AsyncSniffer.start except block until sniffing starts
+
+ This ensures no lost packets and no delays
+ """
+ if sniffer.kwargs.get("started_callback"):
+ raise ValueError("sniffer must not already have a started_callback")
+
+ e = SimpleWaitEvent()
+ sniffer.kwargs["started_callback"] = e.set
+ sniffer.start()
+ e.wait(timeout=timeout)
+
+
+def scapy_sniffer_stop(sniffer: AsyncSniffer):
+ """Like AsyncSniffer.stop except no error is raising if not running"""
+ if sniffer is not None and sniffer.running:
+ sniffer.stop()
+
+
+class AsyncSnifferContext(AsyncSniffer):
+ def __enter__(self):
+ scapy_sniffer_start_block(self)
+ return self
+
+ def __exit__(self, *a):
+ scapy_sniffer_stop(self)
--
2.25.1

2021-08-24 21:36:58

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 13/15] selftests: Add -t tcp_authopt option for fcnal-test.sh

This script is otherwise very slow to run!

Signed-off-by: Leonard Crestez <[email protected]>
---
tools/testing/selftests/net/fcnal-test.sh | 12 ++++++++++++
1 file changed, 12 insertions(+)

diff --git a/tools/testing/selftests/net/fcnal-test.sh b/tools/testing/selftests/net/fcnal-test.sh
index ca3b90f6fecb..3fa812789ac2 100755
--- a/tools/testing/selftests/net/fcnal-test.sh
+++ b/tools/testing/selftests/net/fcnal-test.sh
@@ -1328,10 +1328,21 @@ ipv4_tcp()
log_subsection "With VRF"
setup "yes"
ipv4_tcp_vrf
}

+
+only_tcp_authopt()
+{
+ log_section "TCP Authentication"
+ setup
+ set_sysctl net.ipv4.tcp_l3mdev_accept=0
+ log_subsection "IPv4 no VRF"
+ ipv4_tcp_authopt
+}
+
+
################################################################################
# IPv4 UDP

ipv4_udp_novrf()
{
@@ -4018,10 +4029,11 @@ do
ipv6_bind|bind6) ipv6_addr_bind;;
ipv6_runtime) ipv6_runtime;;
ipv6_netfilter) ipv6_netfilter;;

use_cases) use_cases;;
+ tcp_authopt) only_tcp_authopt;;

# setup namespaces and config, but do not run any tests
setup) setup; exit 0;;
vrf_setup) setup "yes"; exit 0;;

--
2.25.1

2021-08-24 21:37:19

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 14/15] tcp: authopt: Add key selection controls

The RFC requires that TCP can report the keyid and rnextkeyid values
being sent or received, implement this via getsockopt values.

The RFC also requires that user can select the sending key and that the
sending key is automatically switched based on rnextkeyid. These
requirements can conflict so we implement both and add a flag which
specifies if user or peer request takes priority.

Also add an option to control rnextkeyid explicitly from userspace.

Signed-off-by: Leonard Crestez <[email protected]>
---
Documentation/networking/tcp_authopt.rst | 25 ++++++++++
include/net/tcp_authopt.h | 10 ++++
include/uapi/linux/tcp.h | 31 ++++++++++++
net/ipv4/tcp_authopt.c | 60 +++++++++++++++++++++++-
4 files changed, 124 insertions(+), 2 deletions(-)

diff --git a/Documentation/networking/tcp_authopt.rst b/Documentation/networking/tcp_authopt.rst
index 484f66f41ad5..cded87a70d05 100644
--- a/Documentation/networking/tcp_authopt.rst
+++ b/Documentation/networking/tcp_authopt.rst
@@ -35,10 +35,35 @@ Keys can be bound to remote addresses in a way that is similar to TCP_MD5.

RFC5925 requires that key ids do not overlap when tcp identifiers (addr/port)
overlap. This is not enforced by linux, configuring ambiguous keys will result
in packet drops and lost connections.

+Key selection
+-------------
+
+On getsockopt(TCP_AUTHOPT) information is provided about keyid/rnextkeyid in
+the last send packet and about the keyid/rnextkeyd in the last valid received
+packet.
+
+By default the sending keyid is selected to match the "rnextkeyid" value sent
+by the remote side. If that keyid is not available (or for new connections) a
+random matching key is selected.
+
+If the `TCP_AUTHOPT_LOCK_KEYID` is set then the sending key is selected by the
+`tcp_authopt.send_local_id` field and rnextkeyid is ignored. If no key with
+local_id == send_local_id is configured then a random matching key is
+selected.
+
+The current sending key is cached in the socket and will not change unless
+requested by remote rnextkeyid or by setsockopt.
+
+The rnextkeyid value sent on the wire is usually the recv_id of the current
+key used for sending. If the TCP_AUTHOPT_LOCK_RNEXTKEY flag is set in
+`tcp_authopt.flags` the value of `tcp_authopt.send_rnextkeyid` is send
+instead. This can be used to implement smooth rollover: the peer will switch
+its keyid to the received rnextkeyid when it is available.
+
ABI Reference
=============

.. kernel-doc:: include/uapi/linux/tcp.h
:identifiers: tcp_authopt tcp_authopt_flag tcp_authopt_key tcp_authopt_key_flag tcp_authopt_alg
diff --git a/include/net/tcp_authopt.h b/include/net/tcp_authopt.h
index 61db268f36f8..92d0c2333272 100644
--- a/include/net/tcp_authopt.h
+++ b/include/net/tcp_authopt.h
@@ -36,11 +36,21 @@ struct tcp_authopt_key_info {
*/
struct tcp_authopt_info {
/** @head: List of tcp_authopt_key_info */
struct hlist_head head;
struct rcu_head rcu;
+ /**
+ * @send_keyid - Current key used for sending, cached.
+ *
+ * Once a key is found it only changes by user or remote request.
+ */
+ struct tcp_authopt_key_info *send_key;
u32 flags;
+ u8 send_keyid;
+ u8 send_rnextkeyid;
+ u8 recv_keyid;
+ u8 recv_rnextkeyid;
u32 src_isn;
u32 dst_isn;
};

#ifdef CONFIG_TCP_AUTHOPT
diff --git a/include/uapi/linux/tcp.h b/include/uapi/linux/tcp.h
index 575162e7e281..43df8e3cd4cc 100644
--- a/include/uapi/linux/tcp.h
+++ b/include/uapi/linux/tcp.h
@@ -346,10 +346,24 @@ struct tcp_diag_md5sig {

/**
* enum tcp_authopt_flag - flags for `tcp_authopt.flags`
*/
enum tcp_authopt_flag {
+ /**
+ * @TCP_AUTHOPT_FLAG_LOCK_KEYID: keyid controlled by sockopt
+ *
+ * If this is set `tcp_authopt.send_keyid` is used to determined sending
+ * key. Otherwise a key with send_id == recv_rnextkeyid is preferred.
+ */
+ TCP_AUTHOPT_FLAG_LOCK_KEYID = (1 << 0),
+ /**
+ * @TCP_AUTHOPT_FLAG_LOCK_RNEXTKEYID: Override rnextkeyid from userspace
+ *
+ * If this is set then `tcp_authopt.send_rnextkeyid` is sent on outbound
+ * packets. Other the recv_id of the current sending key is sent.
+ */
+ TCP_AUTHOPT_FLAG_LOCK_RNEXTKEYID = (1 << 1),
/**
* @TCP_AUTHOPT_FLAG_REJECT_UNEXPECTED:
* Configure behavior of segments with TCP-AO coming from hosts for which no
* key is configured. The default recommended by RFC is to silently accept
* such connections.
@@ -361,10 +375,27 @@ enum tcp_authopt_flag {
* struct tcp_authopt - Per-socket options related to TCP Authentication Option
*/
struct tcp_authopt {
/** @flags: Combination of &enum tcp_authopt_flag */
__u32 flags;
+ /**
+ * @send_keyid: `tcp_authopt_key.send_id` of preferred send key
+ *
+ * This is only used if `TCP_AUTHOPT_FLAG_LOCK_KEYID` is set.
+ */
+ __u8 send_keyid;
+ /**
+ * @send_rnextkeyid: The rnextkeyid to send in packets
+ *
+ * This is controlled by the user iff TCP_AUTHOPT_FLAG_LOCK_RNEXTKEYID is
+ * set. Otherwise rnextkeyid is the recv_id of the current key.
+ */
+ __u8 send_rnextkeyid;
+ /** @recv_keyid: A recently-received keyid value. Only for getsockopt. */
+ __u8 recv_keyid;
+ /** @recv_rnextkeyid: A recently-received rnextkeyid value. Only for getsockopt. */
+ __u8 recv_rnextkeyid;
};

/**
* enum tcp_authopt_key_flag - flags for `tcp_authopt.flags`
*
diff --git a/net/ipv4/tcp_authopt.c b/net/ipv4/tcp_authopt.c
index 08ca77f01c46..1a80df739fd2 100644
--- a/net/ipv4/tcp_authopt.c
+++ b/net/ipv4/tcp_authopt.c
@@ -255,17 +255,44 @@ struct tcp_authopt_key_info *tcp_authopt_lookup_send(struct tcp_authopt_info *in
*/
struct tcp_authopt_key_info *tcp_authopt_select_key(const struct sock *sk,
const struct sock *addr_sk,
u8 *rnextkeyid)
{
+ struct tcp_authopt_key_info *key, *new_key;
struct tcp_authopt_info *info;

info = rcu_dereference(tcp_sk(sk)->authopt_info);
if (!info)
return NULL;

- return tcp_authopt_lookup_send(info, addr_sk, -1);
+ key = info->send_key;
+ if (info->flags & TCP_AUTHOPT_FLAG_LOCK_KEYID) {
+ int send_keyid = info->send_keyid;
+
+ if (!key || key->send_id != send_keyid)
+ new_key = tcp_authopt_lookup_send(info, addr_sk, send_keyid);
+ } else {
+ if (!key || key->send_id != info->recv_rnextkeyid)
+ new_key = tcp_authopt_lookup_send(info, addr_sk, info->recv_rnextkeyid);
+ }
+ if (!key && !new_key)
+ new_key = tcp_authopt_lookup_send(info, addr_sk, -1);
+
+ // Change current key.
+ if (key != new_key && new_key) {
+ key = new_key;
+ info->send_key = key;
+ }
+
+ if (key) {
+ if (info->flags & TCP_AUTHOPT_FLAG_LOCK_RNEXTKEYID)
+ *rnextkeyid = info->send_rnextkeyid;
+ else
+ *rnextkeyid = info->send_rnextkeyid = key->recv_id;
+ }
+
+ return key;
}

static struct tcp_authopt_info *__tcp_authopt_info_get_or_create(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
@@ -285,10 +312,12 @@ static struct tcp_authopt_info *__tcp_authopt_info_get_or_create(struct sock *sk

return info;
}

#define TCP_AUTHOPT_KNOWN_FLAGS ( \
+ TCP_AUTHOPT_FLAG_LOCK_KEYID | \
+ TCP_AUTHOPT_FLAG_LOCK_RNEXTKEYID | \
TCP_AUTHOPT_FLAG_REJECT_UNEXPECTED)

int tcp_set_authopt(struct sock *sk, sockptr_t optval, unsigned int optlen)
{
struct tcp_authopt opt;
@@ -309,10 +338,14 @@ int tcp_set_authopt(struct sock *sk, sockptr_t optval, unsigned int optlen)
info = __tcp_authopt_info_get_or_create(sk);
if (IS_ERR(info))
return PTR_ERR(info);

info->flags = opt.flags & TCP_AUTHOPT_KNOWN_FLAGS;
+ if (opt.flags & TCP_AUTHOPT_FLAG_LOCK_KEYID)
+ info->send_keyid = opt.send_keyid;
+ if (opt.flags & TCP_AUTHOPT_FLAG_LOCK_RNEXTKEYID)
+ info->send_rnextkeyid = opt.send_rnextkeyid;

return 0;
}

int tcp_get_authopt_val(struct sock *sk, struct tcp_authopt *opt)
@@ -326,10 +359,21 @@ int tcp_get_authopt_val(struct sock *sk, struct tcp_authopt *opt)
info = rcu_dereference_check(tp->authopt_info, lockdep_sock_is_held(sk));
if (!info)
return -EINVAL;

opt->flags = info->flags & TCP_AUTHOPT_KNOWN_FLAGS;
+ /* These keyids might be undefined, for example before connect.
+ * Reporting zero is not strictly correct because there are no reserved
+ * values.
+ */
+ if (info->send_key)
+ opt->send_keyid = info->send_key->send_id;
+ else
+ opt->send_keyid = 0;
+ opt->send_rnextkeyid = info->send_rnextkeyid;
+ opt->recv_keyid = info->recv_keyid;
+ opt->recv_rnextkeyid = info->recv_rnextkeyid;

return 0;
}

static void tcp_authopt_key_free_rcu(struct rcu_head *rcu)
@@ -343,10 +387,12 @@ static void tcp_authopt_key_free_rcu(struct rcu_head *rcu)
static void tcp_authopt_key_del(struct sock *sk,
struct tcp_authopt_info *info,
struct tcp_authopt_key_info *key)
{
hlist_del_rcu(&key->node);
+ if (info->send_key == key)
+ info->send_key = NULL;
atomic_sub(sizeof(*key), &sk->sk_omem_alloc);
call_rcu(&key->rcu, tcp_authopt_key_free_rcu);
}

/* free info and keys but don't touch tp->authopt_info */
@@ -496,10 +542,13 @@ int __tcp_authopt_openreq(struct sock *newsk, const struct sock *oldsk, struct r
return -ENOMEM;

sk_nocaps_add(newsk, NETIF_F_GSO_MASK);
new_info->src_isn = tcp_rsk(req)->snt_isn;
new_info->dst_isn = tcp_rsk(req)->rcv_isn;
+ new_info->send_keyid = old_info->send_keyid;
+ new_info->send_rnextkeyid = old_info->send_rnextkeyid;
+ new_info->flags = old_info->flags;
INIT_HLIST_HEAD(&new_info->head);
err = tcp_authopt_clone_keys(newsk, oldsk, new_info, old_info);
if (err) {
__tcp_authopt_info_free(newsk, new_info);
return err;
@@ -1088,11 +1137,11 @@ int __tcp_authopt_inbound_check(struct sock *sk, struct sk_buff *skb, struct tcp
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTHOPTFAILURE);
net_info_ratelimited("TCP Authentication Unexpected: Rejected\n");
return -EINVAL;
} else {
net_info_ratelimited("TCP Authentication Unexpected: Accepted\n");
- return 0;
+ goto accept;
}
}

/* bad inbound key len */
if (key->maclen + 4 != opt->len)
@@ -1106,7 +1155,14 @@ int __tcp_authopt_inbound_check(struct sock *sk, struct sk_buff *skb, struct tcp
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTHOPTFAILURE);
net_info_ratelimited("TCP Authentication Failed\n");
return -EINVAL;
}

+accept:
+ /* Doing this for all valid packets will results in keyids temporarily
+ * flipping back and forth if packets are reordered or retransmitted.
+ */
+ info->recv_keyid = opt->keyid;
+ info->recv_rnextkeyid = opt->rnextkeyid;
+
return 0;
}
--
2.25.1

2021-08-24 21:37:57

by Leonard Crestez

[permalink] [raw]
Subject: [RFCv3 07/15] tcp: authopt: Hook into tcp core

The tcp_authopt features exposes a minimal interface to the rest of the
TCP stack. Only a few functions are exposed and if the feature is
disabled they return neutral values, avoiding ifdefs in the rest of the
code.

Add calls into tcp authopt from send, receive and accept code.

Signed-off-by: Leonard Crestez <[email protected]>
---
include/net/tcp_authopt.h | 56 +++++++++
net/ipv4/tcp_authopt.c | 246 ++++++++++++++++++++++++++++++++++++++
net/ipv4/tcp_input.c | 17 +++
net/ipv4/tcp_ipv4.c | 3 +
net/ipv4/tcp_minisocks.c | 2 +
net/ipv4/tcp_output.c | 74 +++++++++++-
net/ipv6/tcp_ipv6.c | 4 +
7 files changed, 401 insertions(+), 1 deletion(-)

diff --git a/include/net/tcp_authopt.h b/include/net/tcp_authopt.h
index c9ee2059b442..61db268f36f8 100644
--- a/include/net/tcp_authopt.h
+++ b/include/net/tcp_authopt.h
@@ -21,10 +21,11 @@ struct tcp_authopt_key_info {
/* Wire identifiers */
u8 send_id, recv_id;
u8 alg_id;
u8 keylen;
u8 key[TCP_AUTHOPT_MAXKEYLEN];
+ u8 maclen;
struct sockaddr_storage addr;
struct tcp_authopt_alg_imp *alg;
};

/**
@@ -41,15 +42,53 @@ struct tcp_authopt_info {
u32 src_isn;
u32 dst_isn;
};

#ifdef CONFIG_TCP_AUTHOPT
+struct tcp_authopt_key_info *tcp_authopt_select_key(const struct sock *sk,
+ const struct sock *addr_sk,
+ u8 *rnextkeyid);
void tcp_authopt_clear(struct sock *sk);
int tcp_set_authopt(struct sock *sk, sockptr_t optval, unsigned int optlen);
int tcp_get_authopt_val(struct sock *sk, struct tcp_authopt *key);
int tcp_set_authopt_key(struct sock *sk, sockptr_t optval, unsigned int optlen);
+int tcp_authopt_hash(
+ char *hash_location,
+ struct tcp_authopt_key_info *key,
+ struct sock *sk, struct sk_buff *skb);
+int __tcp_authopt_openreq(struct sock *newsk, const struct sock *oldsk, struct request_sock *req);
+static inline int tcp_authopt_openreq(
+ struct sock *newsk,
+ const struct sock *oldsk,
+ struct request_sock *req)
+{
+ if (!rcu_dereference(tcp_sk(oldsk)->authopt_info))
+ return 0;
+ else
+ return __tcp_authopt_openreq(newsk, oldsk, req);
+}
+int __tcp_authopt_inbound_check(
+ struct sock *sk,
+ struct sk_buff *skb,
+ struct tcp_authopt_info *info);
+static inline int tcp_authopt_inbound_check(struct sock *sk, struct sk_buff *skb)
+{
+ struct tcp_authopt_info *info = rcu_dereference(tcp_sk(sk)->authopt_info);
+
+ if (info)
+ return __tcp_authopt_inbound_check(sk, skb, info);
+ else
+ return 0;
+}
#else
+static inline struct tcp_authopt_key_info *tcp_authopt_select_key(
+ const struct sock *sk,
+ const struct sock *addr_sk,
+ u8 *rnextkeyid)
+{
+ return NULL;
+}
static inline int tcp_set_authopt(struct sock *sk, sockptr_t optval, unsigned int optlen)
{
return -ENOPROTOOPT;
}
static inline int tcp_get_authopt_val(struct sock *sk, struct tcp_authopt *key)
@@ -61,8 +100,25 @@ static inline void tcp_authopt_clear(struct sock *sk)
}
static inline int tcp_set_authopt_key(struct sock *sk, sockptr_t optval, unsigned int optlen)
{
return -ENOPROTOOPT;
}
+static inline int tcp_authopt_hash(
+ char *hash_location,
+ struct tcp_authopt_key_info *key,
+ struct sock *sk, struct sk_buff *skb)
+{
+ return -EINVAL;
+}
+static inline int tcp_authopt_openreq(struct sock *newsk,
+ const struct sock *oldsk,
+ struct request_sock *req)
+{
+ return 0;
+}
+static inline int tcp_authopt_inbound_check(struct sock *sk, struct sk_buff *skb)
+{
+ return 0;
+}
#endif

#endif /* _LINUX_TCP_AUTHOPT_H */
diff --git a/net/ipv4/tcp_authopt.c b/net/ipv4/tcp_authopt.c
index 2a3463ad6896..af777244d098 100644
--- a/net/ipv4/tcp_authopt.c
+++ b/net/ipv4/tcp_authopt.c
@@ -203,10 +203,71 @@ static struct tcp_authopt_key_info *tcp_authopt_key_lookup_exact(const struct so
return key_info;

return NULL;
}

+struct tcp_authopt_key_info *tcp_authopt_lookup_send(struct tcp_authopt_info *info,
+ const struct sock *addr_sk,
+ int send_id)
+{
+ struct tcp_authopt_key_info *result = NULL;
+ struct tcp_authopt_key_info *key;
+
+ hlist_for_each_entry_rcu(key, &info->head, node, 0) {
+ if (send_id >= 0 && key->send_id != send_id)
+ continue;
+ if (key->flags & TCP_AUTHOPT_KEY_ADDR_BIND) {
+ if (addr_sk->sk_family == AF_INET) {
+ struct sockaddr_in *key_addr = (struct sockaddr_in *)&key->addr;
+ const struct in_addr *daddr =
+ (const struct in_addr *)&addr_sk->sk_daddr;
+
+ if (WARN_ON(key_addr->sin_family != AF_INET))
+ continue;
+ if (memcmp(daddr, &key_addr->sin_addr, sizeof(*daddr)))
+ continue;
+ }
+ if (addr_sk->sk_family == AF_INET6) {
+ struct sockaddr_in6 *key_addr = (struct sockaddr_in6 *)&key->addr;
+ const struct in6_addr *daddr = &addr_sk->sk_v6_daddr;
+
+ if (WARN_ON(key_addr->sin6_family != AF_INET6))
+ continue;
+ if (memcmp(daddr, &key_addr->sin6_addr, sizeof(*daddr)))
+ continue;
+ }
+ }
+ if (result && net_ratelimit())
+ pr_warn("ambiguous tcp authentication keys configured for send\n");
+ result = key;
+ }
+
+ return result;
+}
+
+/**
+ * tcp_authopt_select_key - select key for sending
+ *
+ * addr_sk is the sock used for comparing daddr, it is only different from sk in
+ * the synack case.
+ *
+ * Result is protected by RCU and can't be stored, it may only be passed to
+ * tcp_authopt_hash and only under a single rcu_read_lock.
+ */
+struct tcp_authopt_key_info *tcp_authopt_select_key(const struct sock *sk,
+ const struct sock *addr_sk,
+ u8 *rnextkeyid)
+{
+ struct tcp_authopt_info *info;
+
+ info = rcu_dereference(tcp_sk(sk)->authopt_info);
+ if (!info)
+ return NULL;
+
+ return tcp_authopt_lookup_send(info, addr_sk, -1);
+}
+
static struct tcp_authopt_info *__tcp_authopt_info_get_or_create(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct tcp_authopt_info *info;

@@ -387,16 +448,69 @@ int tcp_set_authopt_key(struct sock *sk, sockptr_t optval, unsigned int optlen)
key_info->recv_id = opt.recv_id;
key_info->alg_id = opt.alg;
key_info->alg = alg;
key_info->keylen = opt.keylen;
memcpy(key_info->key, opt.key, opt.keylen);
+ key_info->maclen = alg->maclen;
memcpy(&key_info->addr, &opt.addr, sizeof(key_info->addr));
hlist_add_head_rcu(&key_info->node, &info->head);

return 0;
}

+static int tcp_authopt_clone_keys(struct sock *newsk,
+ const struct sock *oldsk,
+ struct tcp_authopt_info *new_info,
+ struct tcp_authopt_info *old_info)
+{
+ struct tcp_authopt_key_info *old_key;
+ struct tcp_authopt_key_info *new_key;
+
+ hlist_for_each_entry_rcu(old_key, &old_info->head, node, lockdep_sock_is_held(sk)) {
+ new_key = sock_kmalloc(newsk, sizeof(*new_key), GFP_ATOMIC);
+ if (!new_key)
+ return -ENOMEM;
+ memcpy(new_key, old_key, sizeof(*new_key));
+ tcp_authopt_alg_incref(old_key->alg);
+ hlist_add_head_rcu(&new_key->node, &new_info->head);
+ }
+
+ return 0;
+}
+
+/** Called to create accepted sockets.
+ *
+ * Need to copy authopt info from listen socket.
+ */
+int __tcp_authopt_openreq(struct sock *newsk, const struct sock *oldsk, struct request_sock *req)
+{
+ struct tcp_authopt_info *old_info;
+ struct tcp_authopt_info *new_info;
+ int err;
+
+ old_info = rcu_dereference(tcp_sk(oldsk)->authopt_info);
+ if (!old_info)
+ return 0;
+
+ new_info = kmalloc(sizeof(*new_info), GFP_ATOMIC | __GFP_ZERO);
+ if (!new_info)
+ return -ENOMEM;
+
+ sk_nocaps_add(newsk, NETIF_F_GSO_MASK);
+ new_info->src_isn = tcp_rsk(req)->snt_isn;
+ new_info->dst_isn = tcp_rsk(req)->rcv_isn;
+ INIT_HLIST_HEAD(&new_info->head);
+ err = tcp_authopt_clone_keys(newsk, oldsk, new_info, old_info);
+ if (err) {
+ __tcp_authopt_info_free(newsk, new_info);
+ return err;
+ }
+ rcu_assign_pointer(tcp_sk(newsk)->authopt_info, new_info);
+
+ return 0;
+}
+
/* feed traffic key into shash */
static int tcp_authopt_shash_traffic_key(struct shash_desc *desc,
struct sock *sk,
struct sk_buff *skb,
bool input,
@@ -815,10 +929,16 @@ static int tcp_authopt_hash_packet(struct crypto_shash *tfm,
return err;

return crypto_shash_final(desc, macbuf);
}

+/**
+ * __tcp_authopt_calc_mac - Compute packet MAC using key
+ *
+ * @macbuf: output buffer. Must be large enough to fit the digestsize of the
+ * underlying transform before truncation. Please use TCP_AUTHOPT_MAXMACBUF
+ */
int __tcp_authopt_calc_mac(struct sock *sk,
struct sk_buff *skb,
struct tcp_authopt_key_info *key,
bool input,
char *macbuf)
@@ -859,5 +979,131 @@ int __tcp_authopt_calc_mac(struct sock *sk,

out:
tcp_authopt_put_mac_shash(key, mac_tfm);
return err;
}
+
+/**
+ * tcp_authopt_hash - fill in the mac
+ *
+ * The key must come from tcp_authopt_select_key.
+ */
+int tcp_authopt_hash(char *hash_location,
+ struct tcp_authopt_key_info *key,
+ struct sock *sk,
+ struct sk_buff *skb)
+{
+ /* MAC inside option is truncated to 12 bytes but crypto API needs output
+ * buffer to be large enough so we use a buffer on the stack.
+ */
+ u8 macbuf[TCP_AUTHOPT_MAXMACBUF];
+ int err;
+
+ if (WARN_ON(key->maclen > sizeof(macbuf)))
+ return -ENOBUFS;
+
+ err = __tcp_authopt_calc_mac(sk, skb, key, false, macbuf);
+ if (err) {
+ /* If mac calculation fails and caller doesn't handle the error
+ * try to make it obvious inside the packet.
+ */
+ memset(hash_location, 0, key->maclen);
+ return err;
+ }
+ memcpy(hash_location, macbuf, key->maclen);
+
+ return 0;
+}
+
+static struct tcp_authopt_key_info *tcp_authopt_lookup_recv(struct sock *sk,
+ struct sk_buff *skb,
+ struct tcp_authopt_info *info,
+ int recv_id)
+{
+ struct tcp_authopt_key_info *result = NULL;
+ struct tcp_authopt_key_info *key;
+
+ /* multiple matches will cause occasional failures */
+ hlist_for_each_entry_rcu(key, &info->head, node, 0) {
+ if (recv_id >= 0 && key->recv_id != recv_id)
+ continue;
+ if (key->flags & TCP_AUTHOPT_KEY_ADDR_BIND) {
+ if (sk->sk_family == AF_INET) {
+ struct sockaddr_in *key_addr = (struct sockaddr_in *)&key->addr;
+ struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
+
+ if (WARN_ON(key_addr->sin_family != AF_INET))
+ continue;
+ if (WARN_ON(iph->version != 4))
+ continue;
+ if (memcmp(&iph->saddr, &key_addr->sin_addr, sizeof(iph->saddr)))
+ continue;
+ }
+ if (sk->sk_family == AF_INET6) {
+ struct sockaddr_in6 *key_addr = (struct sockaddr_in6 *)&key->addr;
+ struct ipv6hdr *iph = (struct ipv6hdr *)skb_network_header(skb);
+
+ if (WARN_ON(key_addr->sin6_family != AF_INET6))
+ continue;
+ if (WARN_ON(iph->version != 6))
+ continue;
+ if (memcmp(&iph->saddr, &key_addr->sin6_addr, sizeof(iph->saddr)))
+ continue;
+ }
+ }
+ if (result && net_ratelimit())
+ pr_warn("ambiguous tcp authentication keys configured for receive\n");
+ result = key;
+ }
+
+ return result;
+}
+
+int __tcp_authopt_inbound_check(struct sock *sk, struct sk_buff *skb, struct tcp_authopt_info *info)
+{
+ struct tcphdr *th = (struct tcphdr *)skb_transport_header(skb);
+ struct tcphdr_authopt *opt;
+ struct tcp_authopt_key_info *key;
+ u8 macbuf[TCP_AUTHOPT_MAXMACBUF];
+ int err;
+
+ opt = (struct tcphdr_authopt *)tcp_authopt_find_option(th);
+ key = tcp_authopt_lookup_recv(sk, skb, info, opt ? opt->keyid : -1);
+
+ /* nothing found or expected */
+ if (!opt && !key)
+ return 0;
+ if (!opt && key) {
+ net_info_ratelimited("TCP Authentication Missing\n");
+ return -EINVAL;
+ }
+ if (opt && !key) {
+ /* RFC5925 Section 7.3:
+ * A TCP-AO implementation MUST allow for configuration of the behavior
+ * of segments with TCP-AO but that do not match an MKT. The initial
+ * default of this configuration SHOULD be to silently accept such
+ * connections.
+ */
+ if (info->flags & TCP_AUTHOPT_FLAG_REJECT_UNEXPECTED) {
+ net_info_ratelimited("TCP Authentication Unexpected: Rejected\n");
+ return -EINVAL;
+ } else {
+ net_info_ratelimited("TCP Authentication Unexpected: Accepted\n");
+ return 0;
+ }
+ }
+
+ /* bad inbound key len */
+ if (key->maclen + 4 != opt->len)
+ return -EINVAL;
+
+ err = __tcp_authopt_calc_mac(sk, skb, key, true, macbuf);
+ if (err)
+ return err;
+
+ if (memcmp(macbuf, opt->mac, key->maclen)) {
+ net_info_ratelimited("TCP Authentication Failed\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
index 3f7bd7ae7d7a..e0b51b2f747f 100644
--- a/net/ipv4/tcp_input.c
+++ b/net/ipv4/tcp_input.c
@@ -70,10 +70,11 @@
#include <linux/sysctl.h>
#include <linux/kernel.h>
#include <linux/prefetch.h>
#include <net/dst.h>
#include <net/tcp.h>
+#include <net/tcp_authopt.h>
#include <net/inet_common.h>
#include <linux/ipsec.h>
#include <asm/unaligned.h>
#include <linux/errqueue.h>
#include <trace/events/tcp.h>
@@ -5967,18 +5968,34 @@ void tcp_init_transfer(struct sock *sk, int bpf_op, struct sk_buff *skb)
if (!icsk->icsk_ca_initialized)
tcp_init_congestion_control(sk);
tcp_init_buffer_space(sk);
}

+static void tcp_authopt_finish_connect(struct sock *sk, struct sk_buff *skb)
+{
+#ifdef CONFIG_TCP_AUTHOPT
+ struct tcp_authopt_info *info;
+
+ info = rcu_dereference_protected(tcp_sk(sk)->authopt_info, lockdep_sock_is_held(sk));
+ if (!info)
+ return;
+
+ info->src_isn = ntohl(tcp_hdr(skb)->ack_seq) - 1;
+ info->dst_isn = ntohl(tcp_hdr(skb)->seq);
+#endif
+}
+
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);

tcp_set_state(sk, TCP_ESTABLISHED);
icsk->icsk_ack.lrcvtime = tcp_jiffies32;

+ tcp_authopt_finish_connect(sk, skb);
+
if (skb) {
icsk->icsk_af_ops->sk_rx_dst_set(sk, skb);
security_inet_conn_established(sk, skb);
sk_mark_napi_id(sk, skb);
}
diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
index 1348615c7576..a1d39183908c 100644
--- a/net/ipv4/tcp_ipv4.c
+++ b/net/ipv4/tcp_ipv4.c
@@ -2060,10 +2060,13 @@ int tcp_v4_rcv(struct sk_buff *skb)
goto discard_and_relse;

if (tcp_v4_inbound_md5_hash(sk, skb, dif, sdif))
goto discard_and_relse;

+ if (tcp_authopt_inbound_check(sk, skb))
+ goto discard_and_relse;
+
nf_reset_ct(skb);

if (tcp_filter(sk, skb))
goto discard_and_relse;
th = (const struct tcphdr *)skb->data;
diff --git a/net/ipv4/tcp_minisocks.c b/net/ipv4/tcp_minisocks.c
index 0a4f3f16140a..4d7d86547b0e 100644
--- a/net/ipv4/tcp_minisocks.c
+++ b/net/ipv4/tcp_minisocks.c
@@ -24,10 +24,11 @@
#include <linux/slab.h>
#include <linux/sysctl.h>
#include <linux/workqueue.h>
#include <linux/static_key.h>
#include <net/tcp.h>
+#include <net/tcp_authopt.h>
#include <net/inet_common.h>
#include <net/xfrm.h>
#include <net/busy_poll.h>

static bool tcp_in_window(u32 seq, u32 end_seq, u32 s_win, u32 e_win)
@@ -539,10 +540,11 @@ struct sock *tcp_create_openreq_child(const struct sock *sk,
#ifdef CONFIG_TCP_MD5SIG
newtp->md5sig_info = NULL; /*XXX*/
if (newtp->af_specific->md5_lookup(sk, newsk))
newtp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;
#endif
+ tcp_authopt_openreq(newsk, sk, req);
if (skb->len >= TCP_MSS_DEFAULT + newtp->tcp_header_len)
newicsk->icsk_ack.last_seg_size = skb->len - newtp->tcp_header_len;
newtp->rx_opt.mss_clamp = req->mss;
tcp_ecn_openreq_child(newtp, req);
newtp->fastopen_req = NULL;
diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c
index 6d72f3ea48c4..6d73bee349c9 100644
--- a/net/ipv4/tcp_output.c
+++ b/net/ipv4/tcp_output.c
@@ -37,10 +37,11 @@

#define pr_fmt(fmt) "TCP: " fmt

#include <net/tcp.h>
#include <net/mptcp.h>
+#include <net/tcp_authopt.h>

#include <linux/compiler.h>
#include <linux/gfp.h>
#include <linux/module.h>
#include <linux/static_key.h>
@@ -411,10 +412,11 @@ static inline bool tcp_urg_mode(const struct tcp_sock *tp)

#define OPTION_SACK_ADVERTISE (1 << 0)
#define OPTION_TS (1 << 1)
#define OPTION_MD5 (1 << 2)
#define OPTION_WSCALE (1 << 3)
+#define OPTION_AUTHOPT (1 << 4)
#define OPTION_FAST_OPEN_COOKIE (1 << 8)
#define OPTION_SMC (1 << 9)
#define OPTION_MPTCP (1 << 10)

static void smc_options_write(__be32 *ptr, u16 *options)
@@ -435,16 +437,21 @@ static void smc_options_write(__be32 *ptr, u16 *options)
struct tcp_out_options {
u16 options; /* bit field of OPTION_* */
u16 mss; /* 0 to disable */
u8 ws; /* window scale, 0 to disable */
u8 num_sack_blocks; /* number of SACK blocks to include */
- u8 hash_size; /* bytes in hash_location */
u8 bpf_opt_len; /* length of BPF hdr option */
+#ifdef CONFIG_TCP_AUTHOPT
+ u8 authopt_rnextkeyid; /* rnextkey */
+#endif
__u8 *hash_location; /* temporary pointer, overloaded */
__u32 tsval, tsecr; /* need to include OPTION_TS */
struct tcp_fastopen_cookie *fastopen_cookie; /* Fast open cookie */
struct mptcp_out_options mptcp;
+#ifdef CONFIG_TCP_AUTHOPT
+ struct tcp_authopt_key_info *authopt_key;
+#endif
};

static void mptcp_options_write(__be32 *ptr, const struct tcp_sock *tp,
struct tcp_out_options *opts)
{
@@ -617,10 +624,24 @@ static void tcp_options_write(__be32 *ptr, struct tcp_sock *tp,
/* overload cookie hash location */
opts->hash_location = (__u8 *)ptr;
ptr += 4;
}

+#ifdef CONFIG_TCP_AUTHOPT
+ if (unlikely(OPTION_AUTHOPT & options)) {
+ struct tcp_authopt_key_info *key = opts->authopt_key;
+
+ WARN_ON(!key);
+ *ptr++ = htonl((TCPOPT_AUTHOPT << 24) | ((4 + key->maclen) << 16) |
+ (key->send_id << 8) | opts->authopt_rnextkeyid);
+ /* overload cookie hash location */
+ opts->hash_location = (__u8 *)ptr;
+ /* maclen is currently always 12 but try to align nicely anyway. */
+ ptr += (key->maclen + 3) / 4;
+ }
+#endif
+
if (unlikely(opts->mss)) {
*ptr++ = htonl((TCPOPT_MSS << 24) |
(TCPOLEN_MSS << 16) |
opts->mss);
}
@@ -752,10 +773,28 @@ static void mptcp_set_option_cond(const struct request_sock *req,
}
}
}
}

+static int tcp_authopt_init_options(const struct sock *sk,
+ const struct sock *addr_sk,
+ struct tcp_out_options *opts)
+{
+#ifdef CONFIG_TCP_AUTHOPT
+ struct tcp_authopt_key_info *key;
+
+ key = tcp_authopt_select_key(sk, addr_sk, &opts->authopt_rnextkeyid);
+ if (key) {
+ opts->options |= OPTION_AUTHOPT;
+ opts->authopt_key = key;
+ return 4 + key->maclen;
+ }
+#endif
+
+ return 0;
+}
+
/* Compute TCP options for SYN packets. This is not the final
* network wire format yet.
*/
static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
struct tcp_out_options *opts,
@@ -774,10 +813,11 @@ static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
opts->options |= OPTION_MD5;
remaining -= TCPOLEN_MD5SIG_ALIGNED;
}
}
#endif
+ remaining -= tcp_authopt_init_options(sk, sk, opts);

/* We always get an MSS option. The option bytes which will be seen in
* normal data packets should timestamps be used, must be in the MSS
* advertised. But we subtract them from tp->mss_cache so that
* calculations in tcp_sendmsg are simpler etc. So account for this
@@ -862,10 +902,11 @@ static unsigned int tcp_synack_options(const struct sock *sk,
*/
if (synack_type != TCP_SYNACK_COOKIE)
ireq->tstamp_ok &= !ireq->sack_ok;
}
#endif
+ remaining -= tcp_authopt_init_options(sk, req_to_sk(req), opts);

/* We always send an MSS option. */
opts->mss = mss;
remaining -= TCPOLEN_MSS_ALIGNED;

@@ -930,10 +971,11 @@ static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb
opts->options |= OPTION_MD5;
size += TCPOLEN_MD5SIG_ALIGNED;
}
}
#endif
+ size += tcp_authopt_init_options(sk, sk, opts);

if (likely(tp->rx_opt.tstamp_ok)) {
opts->options |= OPTION_TS;
opts->tsval = skb ? tcp_skb_timestamp(skb) + tp->tsoffset : 0;
opts->tsecr = tp->rx_opt.ts_recent;
@@ -1277,10 +1319,14 @@ static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,

inet = inet_sk(sk);
tcb = TCP_SKB_CB(skb);
memset(&opts, 0, sizeof(opts));

+#ifdef CONFIG_TCP_AUTHOPT
+ /* for tcp_authopt_init_options inside tcp_syn_options or tcp_established_options */
+ rcu_read_lock();
+#endif
if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {
tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
} else {
tcp_options_size = tcp_established_options(sk, skb, &opts,
&md5);
@@ -1365,10 +1411,17 @@ static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
sk_nocaps_add(sk, NETIF_F_GSO_MASK);
tp->af_specific->calc_md5_hash(opts.hash_location,
md5, sk, skb);
}
#endif
+#ifdef CONFIG_TCP_AUTHOPT
+ if (opts.authopt_key) {
+ sk_nocaps_add(sk, NETIF_F_GSO_MASK);
+ tcp_authopt_hash(opts.hash_location, opts.authopt_key, sk, skb);
+ }
+ rcu_read_unlock();
+#endif

/* BPF prog is the last one writing header option */
bpf_skops_write_hdr_opt(sk, skb, NULL, NULL, 0, &opts);

INDIRECT_CALL_INET(icsk->icsk_af_ops->send_check,
@@ -1836,12 +1889,21 @@ unsigned int tcp_current_mss(struct sock *sk)
u32 mtu = dst_mtu(dst);
if (mtu != inet_csk(sk)->icsk_pmtu_cookie)
mss_now = tcp_sync_mss(sk, mtu);
}

+#ifdef CONFIG_TCP_AUTHOPT
+ /* Even if the result is not used rcu_read_lock is required when scanning for
+ * tcp authentication keys. Otherwise lockdep will complain.
+ */
+ rcu_read_lock();
+#endif
header_len = tcp_established_options(sk, NULL, &opts, &md5) +
sizeof(struct tcphdr);
+#ifdef CONFIG_TCP_AUTHOPT
+ rcu_read_unlock();
+#endif
/* The mss_cache is sized based on tp->tcp_header_len, which assumes
* some common options. If this is an odd packet (because we have SACK
* blocks etc) then our calculated header_len will be different, and
* we have to adjust mss_now correspondingly */
if (header_len != tp->tcp_header_len) {
@@ -3566,10 +3628,14 @@ struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst,
}

#ifdef CONFIG_TCP_MD5SIG
rcu_read_lock();
md5 = tcp_rsk(req)->af_specific->req_md5_lookup(sk, req_to_sk(req));
+#endif
+#ifdef CONFIG_TCP_AUTHOPT
+ /* for tcp_authopt_init_options inside tcp_synack_options */
+ rcu_read_lock();
#endif
skb_set_hash(skb, tcp_rsk(req)->txhash, PKT_HASH_TYPE_L4);
/* bpf program will be interested in the tcp_flags */
TCP_SKB_CB(skb)->tcp_flags = TCPHDR_SYN | TCPHDR_ACK;
tcp_header_size = tcp_synack_options(sk, req, mss, skb, &opts, md5,
@@ -3603,10 +3669,16 @@ struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst,
if (md5)
tcp_rsk(req)->af_specific->calc_md5_hash(opts.hash_location,
md5, req_to_sk(req), skb);
rcu_read_unlock();
#endif
+#ifdef CONFIG_TCP_AUTHOPT
+ /* If signature fails we do nothing */
+ if (opts.authopt_key)
+ tcp_authopt_hash(opts.hash_location, opts.authopt_key, req_to_sk(req), skb);
+ rcu_read_unlock();
+#endif

bpf_skops_write_hdr_opt((struct sock *)sk, skb, req, syn_skb,
synack_type, &opts);

skb->skb_mstamp_ns = now;
diff --git a/net/ipv6/tcp_ipv6.c b/net/ipv6/tcp_ipv6.c
index 0ce52d46e4f8..51381a9c2bd5 100644
--- a/net/ipv6/tcp_ipv6.c
+++ b/net/ipv6/tcp_ipv6.c
@@ -40,10 +40,11 @@
#include <linux/icmpv6.h>
#include <linux/random.h>
#include <linux/indirect_call_wrapper.h>

#include <net/tcp.h>
+#include <net/tcp_authopt.h>
#include <net/ndisc.h>
#include <net/inet6_hashtables.h>
#include <net/inet6_connection_sock.h>
#include <net/ipv6.h>
#include <net/transp_v6.h>
@@ -1733,10 +1734,13 @@ INDIRECT_CALLABLE_SCOPE int tcp_v6_rcv(struct sk_buff *skb)
goto discard_and_relse;

if (tcp_v6_inbound_md5_hash(sk, skb, dif, sdif))
goto discard_and_relse;

+ if (tcp_authopt_inbound_check(sk, skb))
+ goto discard_and_relse;
+
if (tcp_filter(sk, skb))
goto discard_and_relse;
th = (const struct tcphdr *)skb->data;
hdr = ipv6_hdr(skb);
tcp_v6_fill_cb(skb, hdr, th);
--
2.25.1

2021-08-24 22:59:50

by Eric Dumazet

[permalink] [raw]
Subject: Re: [RFCv3 07/15] tcp: authopt: Hook into tcp core



On 8/24/21 2:34 PM, Leonard Crestez wrote:
> The tcp_authopt features exposes a minimal interface to the rest of the
> TCP stack. Only a few functions are exposed and if the feature is
> disabled they return neutral values, avoiding ifdefs in the rest of the
> code.
>
> Add calls into tcp authopt from send, receive and accept code.
>
> Signed-off-by: Leonard Crestez <[email protected]>
> ---
> include/net/tcp_authopt.h | 56 +++++++++
> net/ipv4/tcp_authopt.c | 246 ++++++++++++++++++++++++++++++++++++++
> net/ipv4/tcp_input.c | 17 +++
> net/ipv4/tcp_ipv4.c | 3 +
> net/ipv4/tcp_minisocks.c | 2 +
> net/ipv4/tcp_output.c | 74 +++++++++++-
> net/ipv6/tcp_ipv6.c | 4 +
> 7 files changed, 401 insertions(+), 1 deletion(-)
>
> diff --git a/include/net/tcp_authopt.h b/include/net/tcp_authopt.h
> index c9ee2059b442..61db268f36f8 100644
> --- a/include/net/tcp_authopt.h
> +++ b/include/net/tcp_authopt.h
> @@ -21,10 +21,11 @@ struct tcp_authopt_key_info {
> /* Wire identifiers */
> u8 send_id, recv_id;
> u8 alg_id;
> u8 keylen;
> u8 key[TCP_AUTHOPT_MAXKEYLEN];
> + u8 maclen;

I do not see maclen being enforced to 12, or a multiple of 4 ?

This means that later [2], tcp_authopt_hash() will leave up to 3
unitialized bytes in the TCP options, sent to the wire.

This is a security issue, since we will leak kernel memory.

> struct sockaddr_storage addr;
> struct tcp_authopt_alg_imp *alg;
> };
>
> /**
> @@ -41,15 +42,53 @@ struct tcp_authopt_info {
> u32 src_isn;
> u32 dst_isn;
> };
>
> #ifdef CONFIG_TCP_AUTHOPT
> +struct tcp_authopt_key_info *tcp_authopt_select_key(const struct sock *sk,
> + const struct sock *addr_sk,
> + u8 *rnextkeyid);
> void tcp_authopt_clear(struct sock *sk);
> int tcp_set_authopt(struct sock *sk, sockptr_t optval, unsigned int optlen);
> int tcp_get_authopt_val(struct sock *sk, struct tcp_authopt *key);
> int tcp_set_authopt_key(struct sock *sk, sockptr_t optval, unsigned int optlen);
> +int tcp_authopt_hash(
> + char *hash_location,
> + struct tcp_authopt_key_info *key,
> + struct sock *sk, struct sk_buff *skb);
> +int __tcp_authopt_openreq(struct sock *newsk, const struct sock *oldsk, struct request_sock *req);
> +static inline int tcp_authopt_openreq(
> + struct sock *newsk,
> + const struct sock *oldsk,
> + struct request_sock *req)
> +{
> + if (!rcu_dereference(tcp_sk(oldsk)->authopt_info))
> + return 0;
> + else
> + return __tcp_authopt_openreq(newsk, oldsk, req);
> +}
> +int __tcp_authopt_inbound_check(
> + struct sock *sk,
> + struct sk_buff *skb,
> + struct tcp_authopt_info *info);
> +static inline int tcp_authopt_inbound_check(struct sock *sk, struct sk_buff *skb)
> +{
> + struct tcp_authopt_info *info = rcu_dereference(tcp_sk(sk)->authopt_info);
> +
> + if (info)
> + return __tcp_authopt_inbound_check(sk, skb, info);
> + else
> + return 0;
> +}
> #else
> +static inline struct tcp_authopt_key_info *tcp_authopt_select_key(
> + const struct sock *sk,
> + const struct sock *addr_sk,
> + u8 *rnextkeyid)
> +{
> + return NULL;
> +}
> static inline int tcp_set_authopt(struct sock *sk, sockptr_t optval, unsigned int optlen)
> {
> return -ENOPROTOOPT;
> }
> static inline int tcp_get_authopt_val(struct sock *sk, struct tcp_authopt *key)
> @@ -61,8 +100,25 @@ static inline void tcp_authopt_clear(struct sock *sk)
> }
> static inline int tcp_set_authopt_key(struct sock *sk, sockptr_t optval, unsigned int optlen)
> {
> return -ENOPROTOOPT;
> }
> +static inline int tcp_authopt_hash(
> + char *hash_location,
> + struct tcp_authopt_key_info *key,
> + struct sock *sk, struct sk_buff *skb)
> +{
> + return -EINVAL;
> +}
> +static inline int tcp_authopt_openreq(struct sock *newsk,
> + const struct sock *oldsk,
> + struct request_sock *req)
> +{
> + return 0;
> +}
> +static inline int tcp_authopt_inbound_check(struct sock *sk, struct sk_buff *skb)
> +{
> + return 0;
> +}
> #endif
>
> #endif /* _LINUX_TCP_AUTHOPT_H */
> diff --git a/net/ipv4/tcp_authopt.c b/net/ipv4/tcp_authopt.c
> index 2a3463ad6896..af777244d098 100644
> --- a/net/ipv4/tcp_authopt.c
> +++ b/net/ipv4/tcp_authopt.c
> @@ -203,10 +203,71 @@ static struct tcp_authopt_key_info *tcp_authopt_key_lookup_exact(const struct so
> return key_info;
>
> return NULL;
> }
>
> +struct tcp_authopt_key_info *tcp_authopt_lookup_send(struct tcp_authopt_info *info,
> + const struct sock *addr_sk,
> + int send_id)
> +{
> + struct tcp_authopt_key_info *result = NULL;
> + struct tcp_authopt_key_info *key;
> +
> + hlist_for_each_entry_rcu(key, &info->head, node, 0) {
> + if (send_id >= 0 && key->send_id != send_id)
> + continue;
> + if (key->flags & TCP_AUTHOPT_KEY_ADDR_BIND) {
> + if (addr_sk->sk_family == AF_INET) {
> + struct sockaddr_in *key_addr = (struct sockaddr_in *)&key->addr;
> + const struct in_addr *daddr =
> + (const struct in_addr *)&addr_sk->sk_daddr;

Why a cast is needed ? sk_daddr is a __be32, no need to cast it to in_addr
> +
> + if (WARN_ON(key_addr->sin_family != AF_INET))

Why a WARN_ON() is used ? If we expect this to trigger, then at minimumum WARN_ON_ONCE() please.

> + continue;
> + if (memcmp(daddr, &key_addr->sin_addr, sizeof(*daddr)))
> + continue;

Using memcmp() to compare two __be32 is overkill.

> + }
> + if (addr_sk->sk_family == AF_INET6) {
> + struct sockaddr_in6 *key_addr = (struct sockaddr_in6 *)&key->addr;
> + const struct in6_addr *daddr = &addr_sk->sk_v6_daddr;

Not sure why a variable is used, you need it once.

> +
> + if (WARN_ON(key_addr->sin6_family != AF_INET6))
> + continue;
> + if (memcmp(daddr, &key_addr->sin6_addr, sizeof(*daddr)))

ipv6_addr_equal() should be faster.

> + continue;
> + }
> + }
> + if (result && net_ratelimit())
> + pr_warn("ambiguous tcp authentication keys configured for send\n");
> + result = key;
> + }
> +
> + return result;
> +}
> +
> +/**
> + * tcp_authopt_select_key - select key for sending
> + *
> + * addr_sk is the sock used for comparing daddr, it is only different from sk in
> + * the synack case.
> + *
> + * Result is protected by RCU and can't be stored, it may only be passed to
> + * tcp_authopt_hash and only under a single rcu_read_lock.
> + */
> +struct tcp_authopt_key_info *tcp_authopt_select_key(const struct sock *sk,
> + const struct sock *addr_sk,
> + u8 *rnextkeyid)
> +{
> + struct tcp_authopt_info *info;
> +
> + info = rcu_dereference(tcp_sk(sk)->authopt_info);

distro kernels will have CONFIG_TCP_AUTHOPT set, meaning
that we will add a cache line miss for every incoming TCP packet
even on hosts not using any RFC5925 TCP flow.

For TCP MD5 we are using a static key, to avoid this extra cost.

> + if (!info)
> + return NULL;
> +
> + return tcp_authopt_lookup_send(info, addr_sk, -1);
> +}
> +
> static struct tcp_authopt_info *__tcp_authopt_info_get_or_create(struct sock *sk)
> {
> struct tcp_sock *tp = tcp_sk(sk);
> struct tcp_authopt_info *info;
>
> @@ -387,16 +448,69 @@ int tcp_set_authopt_key(struct sock *sk, sockptr_t optval, unsigned int optlen)
> key_info->recv_id = opt.recv_id;
> key_info->alg_id = opt.alg;
> key_info->alg = alg;
> key_info->keylen = opt.keylen;
> memcpy(key_info->key, opt.key, opt.keylen);
> + key_info->maclen = alg->maclen;
> memcpy(&key_info->addr, &opt.addr, sizeof(key_info->addr));
> hlist_add_head_rcu(&key_info->node, &info->head);
>
> return 0;
> }
>
> +static int tcp_authopt_clone_keys(struct sock *newsk,
> + const struct sock *oldsk,
> + struct tcp_authopt_info *new_info,
> + struct tcp_authopt_info *old_info)
> +{
> + struct tcp_authopt_key_info *old_key;
> + struct tcp_authopt_key_info *new_key;
> +
> + hlist_for_each_entry_rcu(old_key, &old_info->head, node, lockdep_sock_is_held(sk)) {
> + new_key = sock_kmalloc(newsk, sizeof(*new_key), GFP_ATOMIC);
> + if (!new_key)
> + return -ENOMEM;
> + memcpy(new_key, old_key, sizeof(*new_key));
> + tcp_authopt_alg_incref(old_key->alg);
> + hlist_add_head_rcu(&new_key->node, &new_info->head);
> + }
> +
> + return 0;
> +}
> +
> +/** Called to create accepted sockets.
> + *
> + * Need to copy authopt info from listen socket.
> + */
> +int __tcp_authopt_openreq(struct sock *newsk, const struct sock *oldsk, struct request_sock *req)
> +{
> + struct tcp_authopt_info *old_info;
> + struct tcp_authopt_info *new_info;
> + int err;
> +
> + old_info = rcu_dereference(tcp_sk(oldsk)->authopt_info);
> + if (!old_info)
> + return 0;
> +
> + new_info = kmalloc(sizeof(*new_info), GFP_ATOMIC | __GFP_ZERO);

kzalloc() is your friend. (same remark for your other patches, where you are using __GFP_ZERO)
Also see additional comment [1]

> + if (!new_info)
> + return -ENOMEM;
> +
> + sk_nocaps_add(newsk, NETIF_F_GSO_MASK);
> + new_info->src_isn = tcp_rsk(req)->snt_isn;
> + new_info->dst_isn = tcp_rsk(req)->rcv_isn;
> + INIT_HLIST_HEAD(&new_info->head);
> + err = tcp_authopt_clone_keys(newsk, oldsk, new_info, old_info);
> + if (err) {
> + __tcp_authopt_info_free(newsk, new_info);

[1]
Are we leaving in place old value of newsk->authopt_info ?
If this is copied from the listener, I think you need
to add a tcp_sk(newsk)->authopt_info = NULL;
before the kzalloc() call done above.



> + return err;
> + }
> + rcu_assign_pointer(tcp_sk(newsk)->authopt_info, new_info);
> +
> + return 0;
> +}
> +
> /* feed traffic key into shash */
> static int tcp_authopt_shash_traffic_key(struct shash_desc *desc,
> struct sock *sk,
> struct sk_buff *skb,
> bool input,
> @@ -815,10 +929,16 @@ static int tcp_authopt_hash_packet(struct crypto_shash *tfm,
> return err;
>
> return crypto_shash_final(desc, macbuf);
> }
>
> +/**
> + * __tcp_authopt_calc_mac - Compute packet MAC using key
> + *
> + * @macbuf: output buffer. Must be large enough to fit the digestsize of the
> + * underlying transform before truncation. Please use TCP_AUTHOPT_MAXMACBUF
> + */
> int __tcp_authopt_calc_mac(struct sock *sk,
> struct sk_buff *skb,
> struct tcp_authopt_key_info *key,
> bool input,
> char *macbuf)
> @@ -859,5 +979,131 @@ int __tcp_authopt_calc_mac(struct sock *sk,
>
> out:
> tcp_authopt_put_mac_shash(key, mac_tfm);
> return err;
> }
> +
> +/**
> + * tcp_authopt_hash - fill in the mac
> + *
> + * The key must come from tcp_authopt_select_key.
> + */
> +int tcp_authopt_hash(char *hash_location,
> + struct tcp_authopt_key_info *key,
> + struct sock *sk,
> + struct sk_buff *skb)
> +{
> + /* MAC inside option is truncated to 12 bytes but crypto API needs output
> + * buffer to be large enough so we use a buffer on the stack.
> + */
> + u8 macbuf[TCP_AUTHOPT_MAXMACBUF];
> + int err;
> +
> + if (WARN_ON(key->maclen > sizeof(macbuf)))
> + return -ENOBUFS;
> +
> + err = __tcp_authopt_calc_mac(sk, skb, key, false, macbuf);
> + if (err) {
> + /* If mac calculation fails and caller doesn't handle the error
> + * try to make it obvious inside the packet.
> + */
> + memset(hash_location, 0, key->maclen);
> + return err;
> + }
> + memcpy(hash_location, macbuf, key->maclen);


[2]
This is the place were we do not make sure to clear the padding bytes
(if key->maclen is not a multiple of 4)


> +
> + return 0;
> +}
> +
> +static struct tcp_authopt_key_info *tcp_authopt_lookup_recv(struct sock *sk,
> + struct sk_buff *skb,
> + struct tcp_authopt_info *info,
> + int recv_id)
> +{
> + struct tcp_authopt_key_info *result = NULL;
> + struct tcp_authopt_key_info *key;
> +
> + /* multiple matches will cause occasional failures */
> + hlist_for_each_entry_rcu(key, &info->head, node, 0) {
> + if (recv_id >= 0 && key->recv_id != recv_id)
> + continue;
> + if (key->flags & TCP_AUTHOPT_KEY_ADDR_BIND) {
> + if (sk->sk_family == AF_INET) {
> + struct sockaddr_in *key_addr = (struct sockaddr_in *)&key->addr;
> + struct iphdr *iph = (struct iphdr *)skb_network_header(skb);
> +
> + if (WARN_ON(key_addr->sin_family != AF_INET))
> + continue;
> + if (WARN_ON(iph->version != 4))
> + continue;
> + if (memcmp(&iph->saddr, &key_addr->sin_addr, sizeof(iph->saddr)))
> + continue;
> + }
> + if (sk->sk_family == AF_INET6) {
> + struct sockaddr_in6 *key_addr = (struct sockaddr_in6 *)&key->addr;
> + struct ipv6hdr *iph = (struct ipv6hdr *)skb_network_header(skb);
> +
> + if (WARN_ON(key_addr->sin6_family != AF_INET6))
> + continue;
> + if (WARN_ON(iph->version != 6))
> + continue;
> + if (memcmp(&iph->saddr, &key_addr->sin6_addr, sizeof(iph->saddr)))
> + continue;
> + }
> + }
> + if (result && net_ratelimit())
> + pr_warn("ambiguous tcp authentication keys configured for receive\n");
> + result = key;
> + }
> +
> + return result;
> +}
> +
> +int __tcp_authopt_inbound_check(struct sock *sk, struct sk_buff *skb, struct tcp_authopt_info *info)
> +{
> + struct tcphdr *th = (struct tcphdr *)skb_transport_header(skb);
> + struct tcphdr_authopt *opt;
> + struct tcp_authopt_key_info *key;
> + u8 macbuf[TCP_AUTHOPT_MAXMACBUF];
> + int err;
> +
> + opt = (struct tcphdr_authopt *)tcp_authopt_find_option(th);
> + key = tcp_authopt_lookup_recv(sk, skb, info, opt ? opt->keyid : -1);
> +
> + /* nothing found or expected */
> + if (!opt && !key)
> + return 0;
> + if (!opt && key) {
> + net_info_ratelimited("TCP Authentication Missing\n");
> + return -EINVAL;
> + }
> + if (opt && !key) {
> + /* RFC5925 Section 7.3:
> + * A TCP-AO implementation MUST allow for configuration of the behavior
> + * of segments with TCP-AO but that do not match an MKT. The initial
> + * default of this configuration SHOULD be to silently accept such
> + * connections.
> + */
> + if (info->flags & TCP_AUTHOPT_FLAG_REJECT_UNEXPECTED) {
> + net_info_ratelimited("TCP Authentication Unexpected: Rejected\n");
> + return -EINVAL;
> + } else {
> + net_info_ratelimited("TCP Authentication Unexpected: Accepted\n");
> + return 0;
> + }
> + }
> +
> + /* bad inbound key len */
> + if (key->maclen + 4 != opt->len)
> + return -EINVAL;
> +
> + err = __tcp_authopt_calc_mac(sk, skb, key, true, macbuf);
> + if (err)
> + return err;
> +
> + if (memcmp(macbuf, opt->mac, key->maclen)) {
> + net_info_ratelimited("TCP Authentication Failed\n");
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> diff --git a/net/ipv4/tcp_input.c b/net/ipv4/tcp_input.c
> index 3f7bd7ae7d7a..e0b51b2f747f 100644
> --- a/net/ipv4/tcp_input.c
> +++ b/net/ipv4/tcp_input.c
> @@ -70,10 +70,11 @@
> #include <linux/sysctl.h>
> #include <linux/kernel.h>
> #include <linux/prefetch.h>
> #include <net/dst.h>
> #include <net/tcp.h>
> +#include <net/tcp_authopt.h>
> #include <net/inet_common.h>
> #include <linux/ipsec.h>
> #include <asm/unaligned.h>
> #include <linux/errqueue.h>
> #include <trace/events/tcp.h>
> @@ -5967,18 +5968,34 @@ void tcp_init_transfer(struct sock *sk, int bpf_op, struct sk_buff *skb)
> if (!icsk->icsk_ca_initialized)
> tcp_init_congestion_control(sk);
> tcp_init_buffer_space(sk);
> }
>
> +static void tcp_authopt_finish_connect(struct sock *sk, struct sk_buff *skb)
> +{
> +#ifdef CONFIG_TCP_AUTHOPT
> + struct tcp_authopt_info *info;
> +
> + info = rcu_dereference_protected(tcp_sk(sk)->authopt_info, lockdep_sock_is_held(sk));
> + if (!info)
> + return;
> +
> + info->src_isn = ntohl(tcp_hdr(skb)->ack_seq) - 1;
> + info->dst_isn = ntohl(tcp_hdr(skb)->seq);
> +#endif
> +}
> +
> void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
> {
> struct tcp_sock *tp = tcp_sk(sk);
> struct inet_connection_sock *icsk = inet_csk(sk);
>
> tcp_set_state(sk, TCP_ESTABLISHED);
> icsk->icsk_ack.lrcvtime = tcp_jiffies32;
>
> + tcp_authopt_finish_connect(sk, skb);
> +
> if (skb) {
> icsk->icsk_af_ops->sk_rx_dst_set(sk, skb);
> security_inet_conn_established(sk, skb);
> sk_mark_napi_id(sk, skb);
> }
> diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
> index 1348615c7576..a1d39183908c 100644
> --- a/net/ipv4/tcp_ipv4.c
> +++ b/net/ipv4/tcp_ipv4.c
> @@ -2060,10 +2060,13 @@ int tcp_v4_rcv(struct sk_buff *skb)
> goto discard_and_relse;
>
> if (tcp_v4_inbound_md5_hash(sk, skb, dif, sdif))
> goto discard_and_relse;
>
> + if (tcp_authopt_inbound_check(sk, skb))
> + goto discard_and_relse;
> +
> nf_reset_ct(skb);
>
> if (tcp_filter(sk, skb))
> goto discard_and_relse;
> th = (const struct tcphdr *)skb->data;
> diff --git a/net/ipv4/tcp_minisocks.c b/net/ipv4/tcp_minisocks.c
> index 0a4f3f16140a..4d7d86547b0e 100644
> --- a/net/ipv4/tcp_minisocks.c
> +++ b/net/ipv4/tcp_minisocks.c
> @@ -24,10 +24,11 @@
> #include <linux/slab.h>
> #include <linux/sysctl.h>
> #include <linux/workqueue.h>
> #include <linux/static_key.h>
> #include <net/tcp.h>
> +#include <net/tcp_authopt.h>
> #include <net/inet_common.h>
> #include <net/xfrm.h>
> #include <net/busy_poll.h>
>
> static bool tcp_in_window(u32 seq, u32 end_seq, u32 s_win, u32 e_win)
> @@ -539,10 +540,11 @@ struct sock *tcp_create_openreq_child(const struct sock *sk,
> #ifdef CONFIG_TCP_MD5SIG
> newtp->md5sig_info = NULL; /*XXX*/
> if (newtp->af_specific->md5_lookup(sk, newsk))
> newtp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;
> #endif
> + tcp_authopt_openreq(newsk, sk, req);
> if (skb->len >= TCP_MSS_DEFAULT + newtp->tcp_header_len)
> newicsk->icsk_ack.last_seg_size = skb->len - newtp->tcp_header_len;
> newtp->rx_opt.mss_clamp = req->mss;
> tcp_ecn_openreq_child(newtp, req);
> newtp->fastopen_req = NULL;
> diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c
> index 6d72f3ea48c4..6d73bee349c9 100644
> --- a/net/ipv4/tcp_output.c
> +++ b/net/ipv4/tcp_output.c
> @@ -37,10 +37,11 @@
>
> #define pr_fmt(fmt) "TCP: " fmt
>
> #include <net/tcp.h>
> #include <net/mptcp.h>
> +#include <net/tcp_authopt.h>
>
> #include <linux/compiler.h>
> #include <linux/gfp.h>
> #include <linux/module.h>
> #include <linux/static_key.h>
> @@ -411,10 +412,11 @@ static inline bool tcp_urg_mode(const struct tcp_sock *tp)
>
> #define OPTION_SACK_ADVERTISE (1 << 0)
> #define OPTION_TS (1 << 1)
> #define OPTION_MD5 (1 << 2)
> #define OPTION_WSCALE (1 << 3)
> +#define OPTION_AUTHOPT (1 << 4)
> #define OPTION_FAST_OPEN_COOKIE (1 << 8)
> #define OPTION_SMC (1 << 9)
> #define OPTION_MPTCP (1 << 10)
>
> static void smc_options_write(__be32 *ptr, u16 *options)
> @@ -435,16 +437,21 @@ static void smc_options_write(__be32 *ptr, u16 *options)
> struct tcp_out_options {
> u16 options; /* bit field of OPTION_* */
> u16 mss; /* 0 to disable */
> u8 ws; /* window scale, 0 to disable */
> u8 num_sack_blocks; /* number of SACK blocks to include */
> - u8 hash_size; /* bytes in hash_location */
> u8 bpf_opt_len; /* length of BPF hdr option */
> +#ifdef CONFIG_TCP_AUTHOPT
> + u8 authopt_rnextkeyid; /* rnextkey */
> +#endif
> __u8 *hash_location; /* temporary pointer, overloaded */
> __u32 tsval, tsecr; /* need to include OPTION_TS */
> struct tcp_fastopen_cookie *fastopen_cookie; /* Fast open cookie */
> struct mptcp_out_options mptcp;
> +#ifdef CONFIG_TCP_AUTHOPT
> + struct tcp_authopt_key_info *authopt_key;
> +#endif
> };
>
> static void mptcp_options_write(__be32 *ptr, const struct tcp_sock *tp,
> struct tcp_out_options *opts)
> {
> @@ -617,10 +624,24 @@ static void tcp_options_write(__be32 *ptr, struct tcp_sock *tp,
> /* overload cookie hash location */
> opts->hash_location = (__u8 *)ptr;
> ptr += 4;
> }
>
> +#ifdef CONFIG_TCP_AUTHOPT
> + if (unlikely(OPTION_AUTHOPT & options)) {
> + struct tcp_authopt_key_info *key = opts->authopt_key;
> +
> + WARN_ON(!key);
> + *ptr++ = htonl((TCPOPT_AUTHOPT << 24) | ((4 + key->maclen) << 16) |
> + (key->send_id << 8) | opts->authopt_rnextkeyid);
> + /* overload cookie hash location */
> + opts->hash_location = (__u8 *)ptr;
> + /* maclen is currently always 12 but try to align nicely anyway. */
> + ptr += (key->maclen + 3) / 4;
> + }
> +#endif
> +
> if (unlikely(opts->mss)) {
> *ptr++ = htonl((TCPOPT_MSS << 24) |
> (TCPOLEN_MSS << 16) |
> opts->mss);
> }
> @@ -752,10 +773,28 @@ static void mptcp_set_option_cond(const struct request_sock *req,
> }
> }
> }
> }
>
> +static int tcp_authopt_init_options(const struct sock *sk,
> + const struct sock *addr_sk,
> + struct tcp_out_options *opts)
> +{
> +#ifdef CONFIG_TCP_AUTHOPT
> + struct tcp_authopt_key_info *key;
> +
> + key = tcp_authopt_select_key(sk, addr_sk, &opts->authopt_rnextkeyid);
> + if (key) {
> + opts->options |= OPTION_AUTHOPT;
> + opts->authopt_key = key;
> + return 4 + key->maclen;
> + }
> +#endif
> +
> + return 0;
> +}
> +
> /* Compute TCP options for SYN packets. This is not the final
> * network wire format yet.
> */
> static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
> struct tcp_out_options *opts,
> @@ -774,10 +813,11 @@ static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
> opts->options |= OPTION_MD5;
> remaining -= TCPOLEN_MD5SIG_ALIGNED;
> }
> }
> #endif
> + remaining -= tcp_authopt_init_options(sk, sk, opts);
>
> /* We always get an MSS option. The option bytes which will be seen in
> * normal data packets should timestamps be used, must be in the MSS
> * advertised. But we subtract them from tp->mss_cache so that
> * calculations in tcp_sendmsg are simpler etc. So account for this
> @@ -862,10 +902,11 @@ static unsigned int tcp_synack_options(const struct sock *sk,
> */
> if (synack_type != TCP_SYNACK_COOKIE)
> ireq->tstamp_ok &= !ireq->sack_ok;
> }
> #endif
> + remaining -= tcp_authopt_init_options(sk, req_to_sk(req), opts);
>
> /* We always send an MSS option. */
> opts->mss = mss;
> remaining -= TCPOLEN_MSS_ALIGNED;
>
> @@ -930,10 +971,11 @@ static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb
> opts->options |= OPTION_MD5;
> size += TCPOLEN_MD5SIG_ALIGNED;
> }
> }
> #endif
> + size += tcp_authopt_init_options(sk, sk, opts);
>
> if (likely(tp->rx_opt.tstamp_ok)) {
> opts->options |= OPTION_TS;
> opts->tsval = skb ? tcp_skb_timestamp(skb) + tp->tsoffset : 0;
> opts->tsecr = tp->rx_opt.ts_recent;
> @@ -1277,10 +1319,14 @@ static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
>
> inet = inet_sk(sk);
> tcb = TCP_SKB_CB(skb);
> memset(&opts, 0, sizeof(opts));
>
> +#ifdef CONFIG_TCP_AUTHOPT
> + /* for tcp_authopt_init_options inside tcp_syn_options or tcp_established_options */
> + rcu_read_lock();
> +#endif
> if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {
> tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
> } else {
> tcp_options_size = tcp_established_options(sk, skb, &opts,
> &md5);
> @@ -1365,10 +1411,17 @@ static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
> sk_nocaps_add(sk, NETIF_F_GSO_MASK);
> tp->af_specific->calc_md5_hash(opts.hash_location,
> md5, sk, skb);
> }
> #endif
> +#ifdef CONFIG_TCP_AUTHOPT
> + if (opts.authopt_key) {
> + sk_nocaps_add(sk, NETIF_F_GSO_MASK);
> + tcp_authopt_hash(opts.hash_location, opts.authopt_key, sk, skb);
> + }
> + rcu_read_unlock();
> +#endif
>
> /* BPF prog is the last one writing header option */
> bpf_skops_write_hdr_opt(sk, skb, NULL, NULL, 0, &opts);
>
> INDIRECT_CALL_INET(icsk->icsk_af_ops->send_check,
> @@ -1836,12 +1889,21 @@ unsigned int tcp_current_mss(struct sock *sk)
> u32 mtu = dst_mtu(dst);
> if (mtu != inet_csk(sk)->icsk_pmtu_cookie)
> mss_now = tcp_sync_mss(sk, mtu);
> }
>
> +#ifdef CONFIG_TCP_AUTHOPT
> + /* Even if the result is not used rcu_read_lock is required when scanning for
> + * tcp authentication keys. Otherwise lockdep will complain.
> + */
> + rcu_read_lock();
> +#endif
> header_len = tcp_established_options(sk, NULL, &opts, &md5) +
> sizeof(struct tcphdr);
> +#ifdef CONFIG_TCP_AUTHOPT
> + rcu_read_unlock();
> +#endif
> /* The mss_cache is sized based on tp->tcp_header_len, which assumes
> * some common options. If this is an odd packet (because we have SACK
> * blocks etc) then our calculated header_len will be different, and
> * we have to adjust mss_now correspondingly */
> if (header_len != tp->tcp_header_len) {
> @@ -3566,10 +3628,14 @@ struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst,
> }
>
> #ifdef CONFIG_TCP_MD5SIG
> rcu_read_lock();
> md5 = tcp_rsk(req)->af_specific->req_md5_lookup(sk, req_to_sk(req));
> +#endif
> +#ifdef CONFIG_TCP_AUTHOPT
> + /* for tcp_authopt_init_options inside tcp_synack_options */
> + rcu_read_lock();
> #endif
> skb_set_hash(skb, tcp_rsk(req)->txhash, PKT_HASH_TYPE_L4);
> /* bpf program will be interested in the tcp_flags */
> TCP_SKB_CB(skb)->tcp_flags = TCPHDR_SYN | TCPHDR_ACK;
> tcp_header_size = tcp_synack_options(sk, req, mss, skb, &opts, md5,
> @@ -3603,10 +3669,16 @@ struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst,
> if (md5)
> tcp_rsk(req)->af_specific->calc_md5_hash(opts.hash_location,
> md5, req_to_sk(req), skb);
> rcu_read_unlock();
> #endif
> +#ifdef CONFIG_TCP_AUTHOPT
> + /* If signature fails we do nothing */
> + if (opts.authopt_key)
> + tcp_authopt_hash(opts.hash_location, opts.authopt_key, req_to_sk(req), skb);
> + rcu_read_unlock();
> +#endif
>
> bpf_skops_write_hdr_opt((struct sock *)sk, skb, req, syn_skb,
> synack_type, &opts);
>
> skb->skb_mstamp_ns = now;
> diff --git a/net/ipv6/tcp_ipv6.c b/net/ipv6/tcp_ipv6.c
> index 0ce52d46e4f8..51381a9c2bd5 100644
> --- a/net/ipv6/tcp_ipv6.c
> +++ b/net/ipv6/tcp_ipv6.c
> @@ -40,10 +40,11 @@
> #include <linux/icmpv6.h>
> #include <linux/random.h>
> #include <linux/indirect_call_wrapper.h>
>
> #include <net/tcp.h>
> +#include <net/tcp_authopt.h>
> #include <net/ndisc.h>
> #include <net/inet6_hashtables.h>
> #include <net/inet6_connection_sock.h>
> #include <net/ipv6.h>
> #include <net/transp_v6.h>
> @@ -1733,10 +1734,13 @@ INDIRECT_CALLABLE_SCOPE int tcp_v6_rcv(struct sk_buff *skb)
> goto discard_and_relse;
>
> if (tcp_v6_inbound_md5_hash(sk, skb, dif, sdif))
> goto discard_and_relse;
>
> + if (tcp_authopt_inbound_check(sk, skb))
> + goto discard_and_relse;
> +
> if (tcp_filter(sk, skb))
> goto discard_and_relse;
> th = (const struct tcphdr *)skb->data;
> hdr = ipv6_hdr(skb);
> tcp_v6_fill_cb(skb, hdr, th);
>

2021-08-24 23:03:02

by Eric Dumazet

[permalink] [raw]
Subject: Re: [RFCv3 05/15] tcp: authopt: Add crypto initialization



On 8/24/21 2:34 PM, Leonard Crestez wrote:
> The crypto_shash API is used in order to compute packet signatures. The
> API comes with several unfortunate limitations:
>
> 1) Allocating a crypto_shash can sleep and must be done in user context.
> 2) Packet signatures must be computed in softirq context
> 3) Packet signatures use dynamic "traffic keys" which require exclusive
> access to crypto_shash for crypto_setkey.
>
> The solution is to allocate one crypto_shash for each possible cpu for
> each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
> softirq context, signatures are computed and the tfm is returned.
>
> The pool for each algorithm is reference counted, initialized at
> setsockopt time and released in tcp_authopt_key_info's rcu callback
>
>

I don't know, why should we really care and try so hard to release
the tfm per cpu ?

I would simply allocate them at boot time.
This would avoid the expensive refcounting (potential false sharing)

2021-08-24 23:35:41

by Eric Dumazet

[permalink] [raw]
Subject: Re: [RFCv3 05/15] tcp: authopt: Add crypto initialization



On 8/24/21 2:34 PM, Leonard Crestez wrote:
> The crypto_shash API is used in order to compute packet signatures. The
> API comes with several unfortunate limitations:
>
> 1) Allocating a crypto_shash can sleep and must be done in user context.
> 2) Packet signatures must be computed in softirq context
> 3) Packet signatures use dynamic "traffic keys" which require exclusive
> access to crypto_shash for crypto_setkey.
>
> The solution is to allocate one crypto_shash for each possible cpu for
> each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
> softirq context, signatures are computed and the tfm is returned.
>

I could not see the per-cpu stuff that you mention in the changelog.


2021-08-25 05:18:51

by David Ahern

[permalink] [raw]
Subject: Re: [RFCv3 09/15] selftests: tcp_authopt: Test key address binding

On 8/24/21 2:34 PM, Leonard Crestez wrote:
> By default TCP-AO keys apply to all possible peers but it's possible to
> have different keys for different remote hosts.
>
> This patch adds initial tests for the behavior behind the
> TCP_AUTHOPT_KEY_BIND_ADDR flag. Server rejection is tested via client
> timeout so this can be slightly slow.
>
> Signed-off-by: Leonard Crestez <[email protected]>
> ---
> .../tcp_authopt_test/netns_fixture.py | 63 +++++++
> .../tcp_authopt/tcp_authopt_test/server.py | 82 ++++++++++
> .../tcp_authopt/tcp_authopt_test/test_bind.py | 143 ++++++++++++++++
> .../tcp_authopt/tcp_authopt_test/utils.py | 154 ++++++++++++++++++
> 4 files changed, 442 insertions(+)
> create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/netns_fixture.py
> create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/server.py
> create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_bind.py
> create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/utils.py
>

This should be under selftests/net as a single "tcp_authopt" directory
from what I can tell.

2021-08-25 15:34:34

by Eric Dumazet

[permalink] [raw]
Subject: Re: [RFCv3 05/15] tcp: authopt: Add crypto initialization

On Wed, Aug 25, 2021 at 1:08 AM Herbert Xu <[email protected]> wrote:
>
> On Tue, Aug 24, 2021 at 04:34:58PM -0700, Eric Dumazet wrote:
> >
> > On 8/24/21 2:34 PM, Leonard Crestez wrote:
> > > The crypto_shash API is used in order to compute packet signatures. The
> > > API comes with several unfortunate limitations:
> > >
> > > 1) Allocating a crypto_shash can sleep and must be done in user context.
> > > 2) Packet signatures must be computed in softirq context
> > > 3) Packet signatures use dynamic "traffic keys" which require exclusive
> > > access to crypto_shash for crypto_setkey.
> > >
> > > The solution is to allocate one crypto_shash for each possible cpu for
> > > each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
> > > softirq context, signatures are computed and the tfm is returned.
> > >
> >
> > I could not see the per-cpu stuff that you mention in the changelog.
>
> Perhaps it's time we moved the key information from the tfm into
> the request structure for hashes? Or at least provide a way for
> the key to be in the request structure in addition to the tfm as
> the tfm model still works for IPsec. Ard/Eric, what do you think
> about that?

What is the typical size of a ' tfm' and associated data ?

per-cpu tfm might still make sense, if we had proper NUMA affinities.
AFAIK, currently we can not provide a numa node to crypto allocations.

So using construct like this ends up allocating all data on one single NUMA node

for_each_possible_cpu(cpu) {
tfm = crypto_alloc_shash(algo->name, 0, 0);
if (IS_ERR(tfm))
return PTR_ERR(tfm);
p_tfm = per_cpu_ptr(algo->tfms, cpu);
*p_tfm = tfm;
}

2021-08-25 16:13:13

by Ard Biesheuvel

[permalink] [raw]
Subject: Re: [RFCv3 05/15] tcp: authopt: Add crypto initialization

On Wed, 25 Aug 2021 at 10:08, Herbert Xu <[email protected]> wrote:
>
> On Tue, Aug 24, 2021 at 04:34:58PM -0700, Eric Dumazet wrote:
> >
> > On 8/24/21 2:34 PM, Leonard Crestez wrote:
> > > The crypto_shash API is used in order to compute packet signatures. The
> > > API comes with several unfortunate limitations:
> > >
> > > 1) Allocating a crypto_shash can sleep and must be done in user context.
> > > 2) Packet signatures must be computed in softirq context
> > > 3) Packet signatures use dynamic "traffic keys" which require exclusive
> > > access to crypto_shash for crypto_setkey.
> > >
> > > The solution is to allocate one crypto_shash for each possible cpu for
> > > each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
> > > softirq context, signatures are computed and the tfm is returned.
> > >
> >
> > I could not see the per-cpu stuff that you mention in the changelog.
>
> Perhaps it's time we moved the key information from the tfm into
> the request structure for hashes? Or at least provide a way for
> the key to be in the request structure in addition to the tfm as
> the tfm model still works for IPsec. Ard/Eric, what do you think
> about that?
>

I think it makes sense for a shash desc to have the ability to carry a
key, which will be used instead of the TFM key, but this seems like
quite a lot of work, given that all implementations will need to be
updated. Also, setkey() can currently sleep, so we need to check
whether the existing key manipulation code can actually execute during
init/update/final if sleeping is not permitted.

2021-08-25 16:33:11

by Leonard Crestez

[permalink] [raw]
Subject: Re: [RFCv3 05/15] tcp: authopt: Add crypto initialization



On 8/25/21 7:04 PM, Ard Biesheuvel wrote:
> On Wed, 25 Aug 2021 at 10:08, Herbert Xu <[email protected]> wrote:
>>
>> On Tue, Aug 24, 2021 at 04:34:58PM -0700, Eric Dumazet wrote:
>>>
>>> On 8/24/21 2:34 PM, Leonard Crestez wrote:
>>>> The crypto_shash API is used in order to compute packet signatures. The
>>>> API comes with several unfortunate limitations:
>>>>
>>>> 1) Allocating a crypto_shash can sleep and must be done in user context.
>>>> 2) Packet signatures must be computed in softirq context
>>>> 3) Packet signatures use dynamic "traffic keys" which require exclusive
>>>> access to crypto_shash for crypto_setkey.
>>>>
>>>> The solution is to allocate one crypto_shash for each possible cpu for
>>>> each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
>>>> softirq context, signatures are computed and the tfm is returned.
>>>>
>>>
>>> I could not see the per-cpu stuff that you mention in the changelog.
>>
>> Perhaps it's time we moved the key information from the tfm into
>> the request structure for hashes? Or at least provide a way for
>> the key to be in the request structure in addition to the tfm as
>> the tfm model still works for IPsec. Ard/Eric, what do you think
>> about that?
>>
>
> I think it makes sense for a shash desc to have the ability to carry a
> key, which will be used instead of the TFM key, but this seems like
> quite a lot of work, given that all implementations will need to be
> updated. Also, setkey() can currently sleep, so we need to check
> whether the existing key manipulation code can actually execute during
> init/update/final if sleeping is not permitted.

Are you sure that setkey can sleep? The documentation is not clear,
maybe it only applies to certain hardware implementations?

The TCP Authentication Option needs dynamic keys for SYN and SYNACK
packets, all of which happens in BH context.

--
Regards,
Leonard

2021-08-25 16:33:31

by Leonard Crestez

[permalink] [raw]
Subject: Re: [RFCv3 07/15] tcp: authopt: Hook into tcp core

On 25.08.2021 01:59, Eric Dumazet wrote:
> On 8/24/21 2:34 PM, Leonard Crestez wrote:
>> The tcp_authopt features exposes a minimal interface to the rest of the
>> TCP stack. Only a few functions are exposed and if the feature is
>> disabled they return neutral values, avoiding ifdefs in the rest of the
>> code.
>>
>> Add calls into tcp authopt from send, receive and accept code.
>>
>> Signed-off-by: Leonard Crestez <[email protected]>
>> ---
>> include/net/tcp_authopt.h | 56 +++++++++
>> net/ipv4/tcp_authopt.c | 246 ++++++++++++++++++++++++++++++++++++++
>> net/ipv4/tcp_input.c | 17 +++
>> net/ipv4/tcp_ipv4.c | 3 +
>> net/ipv4/tcp_minisocks.c | 2 +
>> net/ipv4/tcp_output.c | 74 +++++++++++-
>> net/ipv6/tcp_ipv6.c | 4 +
>> 7 files changed, 401 insertions(+), 1 deletion(-)
>>
>> diff --git a/include/net/tcp_authopt.h b/include/net/tcp_authopt.h
>> index c9ee2059b442..61db268f36f8 100644
>> --- a/include/net/tcp_authopt.h
>> +++ b/include/net/tcp_authopt.h
>> @@ -21,10 +21,11 @@ struct tcp_authopt_key_info {
>> /* Wire identifiers */
>> u8 send_id, recv_id;
>> u8 alg_id;
>> u8 keylen;
>> u8 key[TCP_AUTHOPT_MAXKEYLEN];
>> + u8 maclen;
>
> I do not see maclen being enforced to 12, or a multiple of 4 ?

For both current algorithms the maclen value is 12. I just implemented
RFC5926, there is no way to control this from userspace.

> This means that later [2], tcp_authopt_hash() will leave up to 3
> unitialized bytes in the TCP options, sent to the wire.
>
> This is a security issue, since we will leak kernel memory.

Filling the remainder with zeroes does make sense, or at least
WARN_ON(maclen != 4) so that it's obvious to anyone who attempts to
extend the algorithms.

>> +struct tcp_authopt_key_info *tcp_authopt_lookup_send(struct tcp_authopt_info *info,
>> + const struct sock *addr_sk,
>> + int send_id)
>> +{
>> + struct tcp_authopt_key_info *result = NULL;
>> + struct tcp_authopt_key_info *key;
>> +
>> + hlist_for_each_entry_rcu(key, &info->head, node, 0) {
>> + if (send_id >= 0 && key->send_id != send_id)
>> + continue;
>> + if (key->flags & TCP_AUTHOPT_KEY_ADDR_BIND) {
>> + if (addr_sk->sk_family == AF_INET) {
>> + struct sockaddr_in *key_addr = (struct sockaddr_in *)&key->addr;
>> + const struct in_addr *daddr =
>> + (const struct in_addr *)&addr_sk->sk_daddr;
>
> Why a cast is needed ? sk_daddr is a __be32, no need to cast it to in_addr
>> +
>> + if (WARN_ON(key_addr->sin_family != AF_INET))
>
> Why a WARN_ON() is used ? If we expect this to trigger, then at minimumum WARN_ON_ONCE() please.
>
>> + continue;
>> + if (memcmp(daddr, &key_addr->sin_addr, sizeof(*daddr)))
>> + continue;
>
> Using memcmp() to compare two __be32 is overkill.
>
>> + }
>> + if (addr_sk->sk_family == AF_INET6) {
>> + struct sockaddr_in6 *key_addr = (struct sockaddr_in6 *)&key->addr;
>> + const struct in6_addr *daddr = &addr_sk->sk_v6_daddr;
>
> Not sure why a variable is used, you need it once.
>
>> +
>> + if (WARN_ON(key_addr->sin6_family != AF_INET6))
>> + continue;
>> + if (memcmp(daddr, &key_addr->sin6_addr, sizeof(*daddr)))
>
> ipv6_addr_equal() should be faster.

OK, I will replace the comparisons.

Checking address family is mostly paranoia on my part, I don't know if a
real scenario exists for AF mismatch. Still need to check ipv4-mapped
ipv6 addresses, not sure if those can receive ipv4 skbs on an ipv6 socket.

>> +struct tcp_authopt_key_info *tcp_authopt_select_key(const struct sock *sk,
>> + const struct sock *addr_sk,
>> + u8 *rnextkeyid)
>> +{
>> + struct tcp_authopt_info *info;
>> +
>> + info = rcu_dereference(tcp_sk(sk)->authopt_info);
>
> distro kernels will have CONFIG_TCP_AUTHOPT set, meaning
> that we will add a cache line miss for every incoming TCP packet
> even on hosts not using any RFC5925 TCP flow.
>
> For TCP MD5 we are using a static key, to avoid this extra cost.

OK, will add a static_key.

The check for "does socket have tcp_authopt" also belongs in an inline
wrapper, similar to inbound check

>> +int __tcp_authopt_openreq(struct sock *newsk, const struct sock *oldsk, struct request_sock *req)
>> +{
>> + struct tcp_authopt_info *old_info;
>> + struct tcp_authopt_info *new_info;
>> + int err;
>> +
>> + old_info = rcu_dereference(tcp_sk(oldsk)->authopt_info);
>> + if (!old_info)
>> + return 0;
>> +
>> + new_info = kmalloc(sizeof(*new_info), GFP_ATOMIC | __GFP_ZERO);
>
> kzalloc() is your friend. (same remark for your other patches, where you are using __GFP_ZERO)
> Also see additional comment [1]

OK
>
>> + if (!new_info)
>> + return -ENOMEM;
>> +
>> + sk_nocaps_add(newsk, NETIF_F_GSO_MASK);
>> + new_info->src_isn = tcp_rsk(req)->snt_isn;
>> + new_info->dst_isn = tcp_rsk(req)->rcv_isn;
>> + INIT_HLIST_HEAD(&new_info->head);
>> + err = tcp_authopt_clone_keys(newsk, oldsk, new_info, old_info);
>> + if (err) {
>> + __tcp_authopt_info_free(newsk, new_info);
>
> Are we leaving in place old value of newsk->authopt_info ?
> If this is copied from the listener, I think you need
> to add a tcp_sk(newsk)->authopt_info = NULL;
> before the kzalloc() call done above.

Yes, authopt_info should be set to NULL on error because keeping the
listen socket's value is wrong and dangerous (double free).

Leaving authopt_info NULL or malloc failure is still possible dangerous
because it means all keys are ignored and accepted. Not clear how we
could cause tcp_create_openreq_child to fail instead.

This is a problem in a few other parts: if cryptography fails the
outbound MAC is filled with zeros because there's not obvious way to
make TX fail at that point.

>> + err = __tcp_authopt_calc_mac(sk, skb, key, false, macbuf);
>> + if (err) {
>> + /* If mac calculation fails and caller doesn't handle the error
>> + * try to make it obvious inside the packet.
>> + */
>> + memset(hash_location, 0, key->maclen);
>> + return err;
>> + }
>> + memcpy(hash_location, macbuf, key->maclen);
>
>
> [2]
> This is the place were we do not make sure to clear the padding bytes
> (if key->maclen is not a multiple of 4)

Yes. It might make sense to fix in caller because it's the caller which
decides to align options.

2021-08-25 16:36:25

by Leonard Crestez

[permalink] [raw]
Subject: Re: [RFCv3 05/15] tcp: authopt: Add crypto initialization

On 25.08.2021 02:34, Eric Dumazet wrote:
> On 8/24/21 2:34 PM, Leonard Crestez wrote:
>> The crypto_shash API is used in order to compute packet signatures. The
>> API comes with several unfortunate limitations:
>>
>> 1) Allocating a crypto_shash can sleep and must be done in user context.
>> 2) Packet signatures must be computed in softirq context
>> 3) Packet signatures use dynamic "traffic keys" which require exclusive
>> access to crypto_shash for crypto_setkey.
>>
>> The solution is to allocate one crypto_shash for each possible cpu for
>> each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
>> softirq context, signatures are computed and the tfm is returned.
>>
>
> I could not see the per-cpu stuff that you mention in the changelog.

That's a little embarrasing, I forgot to implement the actual per-cpu
stuff. tcp_authopt_alg_imp.tfm is meant to be an array up to NR_CPUS and
tcp_authopt_alg_get_tfm needs no locking other than preempt_disable
(which should already be the case).

The reference counting would still only happen from very few places:
setsockopt, close and openreq. This would only impact request/response
traffic and relatively little.

Performance was not a major focus so far. Preventing impact on non-AO
connections is important but typical AO usecases are long-lived
low-traffic connections.

--
Regards,
Leonard

2021-08-25 16:38:16

by Leonard Crestez

[permalink] [raw]
Subject: Re: [RFCv3 09/15] selftests: tcp_authopt: Test key address binding

On 25.08.2021 08:18, David Ahern wrote:
> On 8/24/21 2:34 PM, Leonard Crestez wrote:
>> By default TCP-AO keys apply to all possible peers but it's possible to
>> have different keys for different remote hosts.
>>
>> This patch adds initial tests for the behavior behind the
>> TCP_AUTHOPT_KEY_BIND_ADDR flag. Server rejection is tested via client
>> timeout so this can be slightly slow.
>>
>> Signed-off-by: Leonard Crestez <[email protected]>
>> ---
>> .../tcp_authopt_test/netns_fixture.py | 63 +++++++
>> .../tcp_authopt/tcp_authopt_test/server.py | 82 ++++++++++
>> .../tcp_authopt/tcp_authopt_test/test_bind.py | 143 ++++++++++++++++
>> .../tcp_authopt/tcp_authopt_test/utils.py | 154 ++++++++++++++++++
>> 4 files changed, 442 insertions(+)
>> create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/netns_fixture.py
>> create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/server.py
>> create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/test_bind.py
>> create mode 100644 tools/testing/selftests/tcp_authopt/tcp_authopt_test/utils.py
>>
>
> This should be under selftests/net as a single "tcp_authopt" directory
> from what I can tell.

Maybe? I found no clear guidelines for organizing tests by subsystem. I
just did a grep for .py in selftests and placed mine next to tc-testing.

Having a tcp_authopt_test code directory under tcp_authopt is the
standard pattern for python packages, otherwise all submodules with
utilities of dubious generality are dumped at the global level. Removing
the tcp_authopt/tcp_authopt_test structure is awkward in python.

One way to deal with this is to add my test code in
tools/testing/selftests/net/tcp_authopt and my setup.cfg and similar
directly in tools/testing/selftests/net. This would make "net" the root
of the package and make it easy to add other networking pytests. This
seems close to what you mean.

kselftest itself does not seem to offer any special support for python
code, only some for C and shell. Maybe it could offer a "kselftest"
package with common utilities that are used by multiple test packages
and everything would be installed into a single virtualenv by makefiles.

--
Regards,
Leonard

2021-08-25 17:56:43

by Eric Dumazet

[permalink] [raw]
Subject: Re: [RFCv3 05/15] tcp: authopt: Add crypto initialization

On Wed, Aug 25, 2021 at 9:35 AM Leonard Crestez <[email protected]> wrote:
>
> On 25.08.2021 02:34, Eric Dumazet wrote:
> > On 8/24/21 2:34 PM, Leonard Crestez wrote:
> >> The crypto_shash API is used in order to compute packet signatures. The
> >> API comes with several unfortunate limitations:
> >>
> >> 1) Allocating a crypto_shash can sleep and must be done in user context.
> >> 2) Packet signatures must be computed in softirq context
> >> 3) Packet signatures use dynamic "traffic keys" which require exclusive
> >> access to crypto_shash for crypto_setkey.
> >>
> >> The solution is to allocate one crypto_shash for each possible cpu for
> >> each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
> >> softirq context, signatures are computed and the tfm is returned.
> >>
> >
> > I could not see the per-cpu stuff that you mention in the changelog.
>
> That's a little embarrasing, I forgot to implement the actual per-cpu
> stuff. tcp_authopt_alg_imp.tfm is meant to be an array up to NR_CPUS and
> tcp_authopt_alg_get_tfm needs no locking other than preempt_disable
> (which should already be the case).

Well, do not use arrays of NR_CPUS and instead use normal per_cpu
accessors (as in __tcp_alloc_md5sig_pool)

>
> The reference counting would still only happen from very few places:
> setsockopt, close and openreq. This would only impact request/response
> traffic and relatively little.

What I meant is that __tcp_alloc_md5sig_pool() allocates stuff one time,
we do not care about tcp_md5sig_pool_populated going back to false.

Otherwise, a single user application constantly allocating a socket,
enabling MD5 (or authopt), then closing the socket would incur
a big cost on hosts with a lot of cpus.

>
> Performance was not a major focus so far. Preventing impact on non-AO
> connections is important but typical AO usecases are long-lived
> low-traffic connections.
>
> --
> Regards,
> Leonard

2021-08-25 21:07:31

by Leonard Crestez

[permalink] [raw]
Subject: Re: [RFCv3 05/15] tcp: authopt: Add crypto initialization

On 8/25/21 8:55 PM, Eric Dumazet wrote:
> On Wed, Aug 25, 2021 at 9:35 AM Leonard Crestez <[email protected]> wrote:
>>
>> On 25.08.2021 02:34, Eric Dumazet wrote:
>>> On 8/24/21 2:34 PM, Leonard Crestez wrote:
>>>> The crypto_shash API is used in order to compute packet signatures. The
>>>> API comes with several unfortunate limitations:
>>>>
>>>> 1) Allocating a crypto_shash can sleep and must be done in user context.
>>>> 2) Packet signatures must be computed in softirq context
>>>> 3) Packet signatures use dynamic "traffic keys" which require exclusive
>>>> access to crypto_shash for crypto_setkey.
>>>>
>>>> The solution is to allocate one crypto_shash for each possible cpu for
>>>> each algorithm at setsockopt time. The per-cpu tfm is then borrowed from
>>>> softirq context, signatures are computed and the tfm is returned.
>>>>
>>>
>>> I could not see the per-cpu stuff that you mention in the changelog.
>>
>> That's a little embarrasing, I forgot to implement the actual per-cpu
>> stuff. tcp_authopt_alg_imp.tfm is meant to be an array up to NR_CPUS and
>> tcp_authopt_alg_get_tfm needs no locking other than preempt_disable
>> (which should already be the case).
>
> Well, do not use arrays of NR_CPUS and instead use normal per_cpu
> accessors (as in __tcp_alloc_md5sig_pool)
>
>>
>> The reference counting would still only happen from very few places:
>> setsockopt, close and openreq. This would only impact request/response
>> traffic and relatively little.
>
> What I meant is that __tcp_alloc_md5sig_pool() allocates stuff one time,
> we do not care about tcp_md5sig_pool_populated going back to false.
>
> Otherwise, a single user application constantly allocating a socket,
> enabling MD5 (or authopt), then closing the socket would incur
> a big cost on hosts with a lot of cpus.

Allocating only once would definitely simply things.

I don't know if this might end up tying hardware resources forever if
some accelerators are in play but for this feature software-only crypto
is perfectly fine.

--
Regards,
Leonard