test(raft): wire-level TLS handshake assertion in tls e2e
This commit is contained in:
@@ -31,6 +31,7 @@ type
|
|||||||
NodeProc = object
|
NodeProc = object
|
||||||
id: string
|
id: string
|
||||||
clientPort: int
|
clientPort: int
|
||||||
|
raftPort: int
|
||||||
p: Process
|
p: Process
|
||||||
dataDir: string
|
dataDir: string
|
||||||
output: string
|
output: string
|
||||||
@@ -67,6 +68,28 @@ proc portOpen(port: int): bool =
|
|||||||
if s != nil: s.close()
|
if s != nil: s.close()
|
||||||
result = false
|
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) =
|
proc killNode(n: var NodeProc) =
|
||||||
if n.p != nil and n.alive:
|
if n.p != nil and n.alive:
|
||||||
try:
|
try:
|
||||||
@@ -159,7 +182,8 @@ proc startNode(id: string, clientPort, raftPort: int, peers, clientPeers: string
|
|||||||
options = {poStdErrToStdOut, poDaemon})
|
options = {poStdErrToStdOut, poDaemon})
|
||||||
discard fcntl(p.outputHandle.cint, F_SETFL,
|
discard fcntl(p.outputHandle.cint, F_SETFL,
|
||||||
fcntl(p.outputHandle.cint, F_GETFL) or O_NONBLOCK)
|
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() =
|
proc runTlsScenario() =
|
||||||
## Fatal phase failures dump all captured node output, record a test
|
## Fatal phase failures dump all captured node output, record a test
|
||||||
@@ -180,16 +204,18 @@ proc runTlsScenario() =
|
|||||||
",n3@127.0.0.1:" & $(cbase + 30)
|
",n3@127.0.0.1:" & $(cbase + 30)
|
||||||
|
|
||||||
var nodes: seq[NodeProc]
|
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
|
# 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
|
# (4th port in each base range). Started later, after the TLS cluster is
|
||||||
# up, so the positive assertions are not polluted by its noise.
|
# up, so the positive assertions are not polluted by its noise.
|
||||||
var rogue: NodeProc
|
var rogue: NodeProc
|
||||||
|
|
||||||
try:
|
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).
|
# Readiness: all three client ports accept TCP connections (10s each).
|
||||||
for i in 0 ..< nodes.len:
|
for i in 0 ..< nodes.len:
|
||||||
let readyStart = getTime()
|
let readyStart = getTime()
|
||||||
@@ -235,6 +261,21 @@ proc runTlsScenario() =
|
|||||||
return
|
return
|
||||||
echo "TLS leader elected: ", nodes[leaderIdx].id, " (term ", leaderTerm, ")"
|
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)
|
let followerIdx = (if leaderIdx == 0: 1 else: 0)
|
||||||
|
|
||||||
# Schema: CREATE TABLE goes through the raft "ddl" log (C3c) — this
|
# Schema: CREATE TABLE goes through the raft "ddl" log (C3c) — this
|
||||||
@@ -319,9 +360,26 @@ proc runTlsScenario() =
|
|||||||
return
|
return
|
||||||
echo "plaintext node n4 started against the TLS peers"
|
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.
|
# Give n4 many election cycles (timeouts 150-300ms) to try its luck.
|
||||||
# The TLS cluster must keep operating among its 3 members meanwhile.
|
# 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
|
# Cluster still elects/operates: leader among the 3 TLS nodes accepts a
|
||||||
# write and it replicates to a follower, with n4 running.
|
# write and it replicates to a follower, with n4 running.
|
||||||
@@ -364,8 +422,6 @@ proc runTlsScenario() =
|
|||||||
fail()
|
fail()
|
||||||
return
|
return
|
||||||
echo "plaintext node never became leader (TLS rejection confirmed)"
|
echo "plaintext node never became leader (TLS rejection confirmed)"
|
||||||
|
|
||||||
check leaderIdx2 >= 0
|
|
||||||
finally:
|
finally:
|
||||||
for n in nodes.mitems:
|
for n in nodes.mitems:
|
||||||
n.killNode()
|
n.killNode()
|
||||||
|
|||||||
Reference in New Issue
Block a user