test(raft): wire-level TLS handshake assertion in tls e2e

This commit is contained in:
2026-07-31 02:05:43 +03:00
parent f2b7ed1ce2
commit f416fe930e
+64 -8
View File
@@ -31,6 +31,7 @@ type
NodeProc = object
id: string
clientPort: int
raftPort: int
p: Process
dataDir: string
output: string
@@ -67,6 +68,28 @@ proc portOpen(port: int): bool =
if s != nil: s.close()
result = false
proc tlsHandshake(port: int): bool =
## True when a real TLS client handshake completes against `port`.
## Wire-level discriminator: against a plaintext peer it fails — either
## immediately (SSL error on the garbage reply) or, when the peer swallows
## our ClientHello and waits for more bytes, via the 3s recv timeout.
var ctx: SslContext
var s: Socket
try:
ctx = newContext(verifyMode = CVerifyNone)
s = newSocket()
var tv = Timeval(tvSec: posix.Time(3), tvUsec: Suseconds(0))
discard setsockopt(s.getFd(), SOL_SOCKET, SO_RCVTIMEO,
addr tv, SockLen(sizeof(tv)))
s.connect("127.0.0.1", Port(port), timeout = 3000)
ctx.wrapConnectedSocket(s, handshakeAsClient)
result = true
except CatchableError:
result = false
finally:
if s != nil: s.close()
if ctx != nil: ctx.destroyContext()
proc killNode(n: var NodeProc) =
if n.p != nil and n.alive:
try:
@@ -159,7 +182,8 @@ proc startNode(id: string, clientPort, raftPort: int, peers, clientPeers: string
options = {poStdErrToStdOut, poDaemon})
discard fcntl(p.outputHandle.cint, F_SETFL,
fcntl(p.outputHandle.cint, F_GETFL) or O_NONBLOCK)
NodeProc(id: id, clientPort: clientPort, p: p, dataDir: dataDir, alive: true)
NodeProc(id: id, clientPort: clientPort, raftPort: raftPort, p: p,
dataDir: dataDir, alive: true)
proc runTlsScenario() =
## Fatal phase failures dump all captured node output, record a test
@@ -180,16 +204,18 @@ proc runTlsScenario() =
",n3@127.0.0.1:" & $(cbase + 30)
var nodes: seq[NodeProc]
for i in 1 .. 3:
nodes.add startNode("n" & $i, cbase + i * 10, rbase + i,
peers, clientPeers, tlsEnabled = true)
# Negative-case node: same peers, raft TLS DISABLED, own dir and ports
# (4th port in each base range). Started later, after the TLS cluster is
# up, so the positive assertions are not polluted by its noise.
var rogue: NodeProc
try:
# Node starts live inside the try so a raise from start #2/#3 still
# reaches the cleanup in the finally below.
for i in 1 .. 3:
nodes.add startNode("n" & $i, cbase + i * 10, rbase + i,
peers, clientPeers, tlsEnabled = true)
# Readiness: all three client ports accept TCP connections (10s each).
for i in 0 ..< nodes.len:
let readyStart = getTime()
@@ -235,6 +261,21 @@ proc runTlsScenario() =
return
echo "TLS leader elected: ", nodes[leaderIdx].id, " (term ", leaderTerm, ")"
# Wire-level TLS assertion: a real client handshake must complete
# against every cluster node's raft port. Without this, the negative
# "plaintext node never becomes leader" check alone cannot distinguish
# TLS rejection from "can't win an election anyway" — if TLS wrapping
# were silently dropped from the transport, these handshakes raise and
# the suite fails.
for n in nodes.items:
if not tlsHandshake(n.raftPort):
echo "TLS handshake failed against raft port of ", n.id,
" (port ", n.raftPort, ") — raft transport not encrypted?"
dumpAll(nodes)
fail()
return
echo "TLS handshake verified against all 3 raft ports"
let followerIdx = (if leaderIdx == 0: 1 else: 0)
# Schema: CREATE TABLE goes through the raft "ddl" log (C3c) — this
@@ -319,9 +360,26 @@ proc runTlsScenario() =
return
echo "plaintext node n4 started against the TLS peers"
# Complementary wire-level negative: the rogue node's raft port speaks
# plaintext, so a TLS handshake against it must FAIL.
if tlsHandshake(rogue.raftPort):
echo "TLS handshake unexpectedly succeeded against plaintext n4 ",
"raft port ", rogue.raftPort
dumpAll(nodes)
fail()
return
echo "TLS handshake correctly fails against plaintext n4 raft port"
# Give n4 many election cycles (timeouts 150-300ms) to try its luck.
# The TLS cluster must keep operating among its 3 members meanwhile.
nodes.drainFor(6000)
# Drain n4's pipe alongside the others — its connection-refused spam
# must not fill the pipe and block the child.
block:
let negStart = getTime()
while getTime() - negStart < initDuration(seconds = 6):
nodes.drainAll()
rogue.drainOutput()
sleep(50)
# Cluster still elects/operates: leader among the 3 TLS nodes accepts a
# write and it replicates to a follower, with n4 running.
@@ -364,8 +422,6 @@ proc runTlsScenario() =
fail()
return
echo "plaintext node never became leader (TLS rejection confirmed)"
check leaderIdx2 >= 0
finally:
for n in nodes.mitems:
n.killNode()